Kotlin使用MockK mock internal val接口变量报错如何解决
编写Kotlin类单元测试时,待测试类中定义了internal val修饰、直接赋值实现类实例的接口类型变量,待测试类代码如下:
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val vehicleName: IVehicleNames = VehicleNameManager
测试代码中尝试通过@RelaxedMockK注解声明同类型Mock实例:
@RelaxedMockK private lateinit var vehicleName: IVehicleNames
运行测试抛出java.lang.ExceptionInInitializerError异常,尝试mockkObject方案未解决问题。
internal val修饰的属性会在待测试类初始化阶段直接赋值为真实的VehicleNameManager实例,测试类中声明的同名Mock变量属于测试类自身作用域,不会自动替换待测试类内部的属性引用。如果VehicleNameManager初始化依赖Android上下文、其他未Mock的全局实例,初始化流程就会失败抛出异常。@VisibleForTesting仅做代码可见性的提示约束,不会修改属性的赋值逻辑,也不会自动关联测试类中的同名Mock变量。
方案1:反射替换待测试类属性(适配现有代码,改动最小)
该属性已经标注@VisibleForTesting,本身就预留了测试访问入口,直接在测试初始化阶段通过反射把待测试类实例中的vehicleName替换为Mock实例即可:
private lateinit var target: YourTargetClassName @RelaxedMockK private lateinit var mockVehicleName: IVehicleNames @Before fun setup() { MockKAnnotations.init(this) target = YourTargetClassName() // 反射替换类成员属性 val field = YourTargetClassName::class.java.getDeclaredField("vehicleName") field.isAccessible = true field.set(target, mockVehicleName) }
注意:Mock变量不要和目标类属性同名,建议加mock前缀避免作用域混淆。如果vehicleName是文件级的顶级属性而非类成员,反射时需要找对应文件生成的xxxKt类的静态字段,传入null作为set方法的实例参数即可。
方案2:改造为构造注入(符合可测试性规范,长期最优)
把硬编码的属性赋值改为带默认值的构造参数,生产代码调用无需改动,测试时可直接传入Mock实例,不需要反射:
// 待测试类改造 class YourTargetClassName( internal val vehicleName: IVehicleNames = VehicleNameManager )
测试代码直接传入Mock即可:
@RelaxedMockK private lateinit var mockVehicleName: IVehicleNames private lateinit var target: YourTargetClassName @Before fun setup() { MockKAnnotations.init(this) target = YourTargetClassName(vehicleName = mockVehicleName) }
方案3:Mock真实实现类初始化逻辑(无需改动业务代码)
如果不想修改业务代码,也不想用反射,需要在目标类初始化前先Mock VehicleNameManager的初始化逻辑,阻止真实初始化流程执行:
@Before fun setup() { MockKAnnotations.init(this) mockkObject(VehicleNameManager) // 定位初始化异常的具体触发点,针对性Mock对应调用 // 例如初始化时调用了initConfig方法,就Mock该方法 every { VehicleNameManager.initConfig(any()) } returns Unit target = YourTargetClassName() }
该方案需要先查看异常堆栈,定位VehicleNameManager静态初始化块中具体哪行代码触发了异常,再针对性Mock对应调用,适配成本高于前两个方案。
避坑提示:不要试图仅在测试类声明同名
@RelaxedMockK变量替换目标类内部属性,二者分属不同类的作用域,没有任何关联,这也是之前代码不生效的核心原因。
内容的提问来源于stack exchange,提问作者Krishna

