1. 简介

单元测试中,Mock(模拟) 是不可或缺的技术手段。它帮助我们隔离被测组件,确保代码各部分独立运行时行为符合预期。在 Kotlin 生态中,MockK 是一个功能强大的测试框架,提供了 Spy(间谍对象) 的能力 —— 它允许我们在真实对象的基础上进行监控和部分模拟。

本文将深入讲解 MockK 中 Spy 的使用场景与技巧,帮助你写出更精准、更可控的单元测试。

✅ 小贴士:Spy 特别适合“大部分走真实逻辑,只对个别方法打桩”的测试场景,避免过度 Mock 导致测试失真。


2. Mock 与 Spy 的区别

在进入 Spy 之前,先快速回顾一下 Mock 的概念:

  • Mock:创建一个完全虚拟的对象,所有方法默认不执行真实逻辑,需要手动定义行为(如返回值、抛异常等)。
  • Spy:基于一个真实对象创建,默认调用原方法实现,但可以对特定方法进行拦截和打桩。

2.1 创建方式对比

类型 创建函数 行为
Mock mockk<T>() 所有方法均需手动 stub,否则返回默认值(如 null0
Spy spyk<T>() 方法默认走真实逻辑,可选择性地对某些方法 mock
val mock = mockk<Calculator>()  // 完全虚拟
val spy = spyk<Calculator>()   // 基于真实实例,可部分拦截

2.2 MockK 依赖配置

要在项目中使用 MockK,需在 build.gradle.kts 中添加依赖:

dependencies {
    testImplementation("io.mockk:mockk:1.13.8")
}

⚠️ 注意版本兼容性,建议使用与 Kotlin 版本匹配的最新稳定版。


3. 在 Kotlin 中使用 Spy

假设我们有一个简单的 Calculator 类:

class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }

    fun findAverage(a: Int, b: Int): Int {
        val total = add(a, b)
        return total / 2
    }
}

现在我们要测试 findAverage() 是否正确调用了 add(),但又不想完全 Mock 整个类。

✅ 正确做法是使用 spyk 创建间谍对象:

class CalculatorTest {

    @Test
    fun testSpy() {
        val spy = spyk<Calculator>()
        
        val result = spy.findAverage(5, 5)
        
        verify { spy.add(5, 5) }  // 验证 add 被调用
        assertEquals(5, result)   // 结果应为 (5+5)/2 = 5
    }
}

📌 关键点:

  • spy.findAverage(5, 5) 会执行真实逻辑;
  • verify { spy.add(5, 5) } 成功捕获了内部方法调用;
  • 不需要手动 stub findAverage,因为它本身没有副作用。

这比直接 Mock 更贴近真实运行环境,避免“测试通过但线上出错”的坑。


4. 使用 Spy 实现部分 Mock

Spy 最强大的地方在于支持 部分 Mock(Partial Mocking):我们可以只替换某个方法的行为,其余保持原样。

继续上面的例子,如果我们想验证当 add() 返回固定值时,findAverage() 的结果是否正确:

@Test
fun testPartialMocking() {
    val spy = spyk<Calculator>()
    
    every { spy.add(any(), any()) } returns 2
    
    val result = spy.findAverage(5, 5)
    
    verify { spy.add(5, 5) }
    assertEquals(1, result)  // (2)/2 = 1
}

✅ 解析:

  • every { ... } returns ... 是 MockK 的 DSL,用于定义方法桩;
  • any() 匹配任意参数;
  • findAverage() 仍执行真实逻辑,只是其依赖的 add() 被替换了。

⚠️ 踩坑提醒:如果方法是 private 或未被 open(Kotlin 默认 final),spyk 可能无法拦截!
👉 解决方案:将类或方法声明为 open,或使用 @RelaxedMockK + 构造器注入等方式绕过限制。


5. 重置 Spy 状态

在一个测试类中,若多个测试共用同一个 Spy 实例,或希望清除之前的 mock 行为和调用记录,可以使用 clearMocks()

@Test
fun testPartialMocking() {
    val spy = spyk<Calculator>()
    
    every { spy.add(any(), any()) } returns 2
    val result = spy.findAverage(5, 5)
    
    verify { spy.add(5, 5) }
    assertEquals(1, result)
    
    clearMocks(spy)  // 清除所有 stub 和调用历史
}

clearMocks() 的作用包括:

  • 移除通过 every { } returns 设置的返回值;
  • 清空 verify 可检查的调用记录;
  • 让 Spy 回到“仅代理真实对象”的初始状态。

适用于:

  • 多个测试方法复用同一 spy 实例;
  • 避免前一个测试影响后一个测试的验证结果。

6. 使用 Spy 的优势

优势 说明
✅ 更真实的测试环境 大部分逻辑走真实实现,减少 Mock 带来的“虚假成功”风险
✅ 精准控制粒度 只 Mock 关键路径,保留周边逻辑完整性
✅ 提升测试可读性 明确表达“这里我只想改这个方法”的意图
✅ 支持集成式验证 可同时验证方法调用顺序、参数、以及最终输出

🎯 适用场景举例:

  • 测试一个调用了数据库的方法,但想 mock 掉发送邮件的部分;
  • 验证某个服务内部是否正确调用了工具类的关键方法;
  • 第三方 SDK 封装类的测试,部分网络请求打桩,本地逻辑保留。

❌ 不推荐滥用:

  • 如果整个类都被大量 mock,不如直接用 mockk
  • 对 final 类或 private 方法,Spy 可能无效(受限于字节码增强机制)。

7. 总结

Spy 是 MockK 提供的一个高级特性,特别适合需要“局部打桩 + 全局真实执行”的测试场景。相比完全 Mock,它更贴近实际运行流程,有助于发现集成问题。

合理使用 Spy,能让你的测试既具备隔离性,又不失真实性,真正做到“测得准、信得过”。

所有示例代码已托管至 GitHub:https://github.com/baeldung/kotlin-tutorials/tree/master/kotlin-mockito


原始标题:Using Spy in MockK