You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Mockk spy类时匿名类字段引用原始非spy对象该如何解决?

问题产生原因

  1. Mockk 的 spyk(实例) 是基于动态代理实现的:生成的 spy 对象是继承自目标类的代理类实例,持有传入的原始实例引用,默认将方法调用转发给原始实例执行。
  2. 原始实例在构造过程中,内部的匿名类已经在编译期绑定了对原始实例的外部引用(匿名类中的 this@MyClass 指向原始实例,且该引用是匿名类的 final 字段,运行期无法修改)。
  3. 调用 spyMyClass.myStringWrapper.setString() 时,匿名类内部对 myString 的赋值操作实际作用在原始实例的字段上,而非 spy 对象本身的字段。
  4. 从 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 00:36:03