使用Mockk spy类时匿名类字段引用原始非spy对象该如何解决?
问题产生原因
- Mockk 的
spyk(实例)是基于动态代理实现的:生成的 spy 对象是继承自目标类的代理类实例,持有传入的原始实例引用,默认将方法调用转发给原始实例执行。 - 原始实例在构造过程中,内部的匿名类已经在编译期绑定了对原始实例的外部引用(匿名类中的
this@MyClass指向原始实例,且该引用是匿名类的 final 字段,运行期无法修改)。 - 调用
spyMyClass.myStringWrapper.setString()时,匿名类内部对myString的赋值操作实际作用在原始实例的字段上,而非 spy 对象本身的字段。 - 从 spy 对象读取
myString时,如果代理类的属性读取逻辑未同步原始实例的字段值,就会返回 null,和实际已经赋值的原始实例字段完全隔离。你的运行日志中匿名类输出的外部类 hash 与 spy 对象 hash 不一致,就是该原因的直接体现。
解决方案
方案1:使用无参 spyk 构造(优先推荐)
如果你要 spy 的类有无参构造方法,直接使用无参重载构造 spy 对象,此时代理类会自行完成父类初始化逻辑,内部匿名类持有的外部引用就是 spy 对象本身,不会出现引用不一致问题:
// 原写法 val spyMyClass = spyk(MyClass()) 改为: val spyMyClass = spyk<MyClass>()
方案2:显式持有原始实例读取值
如果你必须通过传入原始实例的方式构造 spy,可以单独保存原始实例,需要读取字段值时直接读取原始实例的对应字段:
@Test fun testSpyAndRealMyClassStringWrapper2() { val realMyClass = MyClass() println("realMyClass hash is ${realMyClass.hashCode()}") realMyClass.myStringWrapper.setString("Hello World") println("realMyClass myString value ${realMyClass.myString}") println() // 单独保存原始实例 val originInstance = MyClass() val spyMyClass = spyk(originInstance) println("spyMyClass hash is ${spyMyClass.hashCode()}") spyMyClass.myStringWrapper.setString("Hello World") // 读取原始实例的字段即可拿到正确值 println("myString value ${originInstance.myString}") }
方案3:修改匿名类实现,改为调用外部类公开方法
调整业务代码,匿名类内部不直接访问外部类属性,改为调用外部类的公开方法,配合方案1使用时,可以保证 spy 对方法的打桩逻辑正常生效:
// MyClass 内部的匿名类修改为 val myStringWrapper: StringWrapper = object : StringWrapper { val timestamp = System.currentTimeMillis() override fun setString(string: String) { println("myStringWrapper ${this.hashCode()} for ${this@MyClass.hashCode()}") // 改为调用外部类公开方法,而非直接赋值属性 this@MyClass.setString(string) } }
方案4:将匿名类改为独立类,显式传入外部依赖
如果业务允许,将匿名类改为独立的普通类,构造时显式传入外部类实例,构造 spy 后可以手动替换依赖为 spy 对象本身,彻底解决引用不一致问题:
// 独立的Wrapper实现类 class MyStringWrapper(private val myClass: MyClass) : MyClass.StringWrapper { val timestamp = System.currentTimeMillis() override fun setString(string: String) { myClass.setString(string) } } // MyClass 调整为 class MyClass { var myString: String? = null set(value) { println("myString set to ${value?.hashCode()} for ${this@MyClass.hashCode()}") field = value } // 改为var,支持替换 var myStringWrapper: StringWrapper = MyStringWrapper(this) fun setString(string: String) { myString = string } interface StringWrapper { fun setString(string: String) } } // 测试时构造spy后替换依赖 val originInstance = MyClass() val spyMyClass = spyk(originInstance) spyMyClass.myStringWrapper = MyStringWrapper(spyMyClass)
内容的提问来源于stack exchange,提问作者the.joeba
相关产品推荐
相关产品推荐

