1. 简介
单元测试中,Mock(模拟) 是不可或缺的技术手段。它帮助我们隔离被测组件,确保代码各部分独立运行时行为符合预期。在 Kotlin 生态中,MockK 是一个功能强大的测试框架,提供了 Spy(间谍对象) 的能力 —— 它允许我们在真实对象的基础上进行监控和部分模拟。
本文将深入讲解 MockK 中 Spy 的使用场景与技巧,帮助你写出更精准、更可控的单元测试。
✅ 小贴士:Spy 特别适合“大部分走真实逻辑,只对个别方法打桩”的测试场景,避免过度 Mock 导致测试失真。
2. Mock 与 Spy 的区别
在进入 Spy 之前,先快速回顾一下 Mock 的概念:
- Mock:创建一个完全虚拟的对象,所有方法默认不执行真实逻辑,需要手动定义行为(如返回值、抛异常等)。
- Spy:基于一个真实对象创建,默认调用原方法实现,但可以对特定方法进行拦截和打桩。
2.1 创建方式对比
类型 | 创建函数 | 行为 |
---|---|---|
Mock | mockk<T>() |
所有方法均需手动 stub,否则返回默认值(如 null 、0 ) |
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