第一个MockK测试:创建Mock对象、基本验证
好,咱们直接动手。这一章我会带你写出第一个真正的 MockK 测试——不是概念演示,是你日常工作中每天都可能写的测试。我个人觉得,理解创建 Mock 和基本验证这两件事,等于给后续所有的回归测试打好了地基。
本讲核心:掌握三种创建 Mock 对象的姿势,学会用 verify 和 confirmVerified 做最基础的行为验证。
为什么需要 Mock 对象?
你先想想看:一个类要测试,它很可能调了数据库、调了远程服务、调了文件系统。你写单元测试总不能真的去连数据库吧?那测试就变成集成测试了,又慢又不稳定。Mock 的作用就是造一个“替身”,让它替你去响应方法调用,你只管验证它被调过、调了几次、传了什么参数。
我之前在一个支付项目里重构过一段老代码,里面直接引用了三个外部服务。如果不 Mock,写个单元测试得先搭整套环境,一跑就是十几秒。后来全改用 MockK,测试跑起来几乎没感觉——这才是理想的回归测试状态。
创建 Mock 对象的三种方式
MockK 给了你几种创建 Mock 的方式,我按常用程度排个序。
| 方式 | 适用场景 | 示例 |
|---|---|---|
mockk<T>() |
通用 Mock,对类和接口都适用 | val service = mockk<UserService>() |
mockkClass(T::class) |
需要明确指定类的时候,尤其泛型擦除场景 | val repo = mockkClass(Repository::class) |
spyk() |
部分 Mock,保留真实对象的部分行为 | val obj = spyk(RealObject()) |
最常用的就是第一种 mockk<T>()。它内部其实也是调 mockkClass,但语法更简洁。我一般只在遇到 inline class 或者某些泛型问题时才显式使用第二种。
💡 小技巧:如果你 Mock 的是 Kotlin 的接口,mockk<MyInterface>() 完全够用。如果是 Java 类或者第三方库里的抽象类,建议用 mockkClass(MyClass::class) 以免类型推断出问题。
代码示例:创建 Mock 并进行基本验证
来,直接看代码。假设我们有个 UserService 接口,里面有个方法 getUserName(id: Int): String。
// 引入 MockK 和 JUnit5
import io.mockk.*
import org.junit.jupiter.api.Test
interface UserService {
fun getUserName(id: Int): String
}
class UserServiceTest {
// 创建 Mock 对象
private val userService = mockk<UserService>()
@Test
fun `should call getUserName once with correct id`() {
// 1. 定义行为:当 id=1 时返回 "Alice"
every { userService.getUserName(1) } returns "Alice"
// 2. 执行被测试代码(这里直接调用模拟一下真实逻辑)
val result = userService.getUserName(1)
// 3. 验证:getUserName 被调用了正好 1 次,参数是 1
verify(exactly = 1) { userService.getUserName(1) }
// 4. 可选:确认没有其他未验证的调用
confirmVerified(userService)
}
}
这段代码涵盖了几个关键步骤:every 定义行为、验证调用了多少次、确认没有多余的调用。我刚开始用 MockK 时,经常忘记写 confirmVerified,结果测试虽然过了,但被测代码里其实多调了一个无关方法——这是非常危险的遗漏。
verify 的几种常用模式
verify 不止能检查「调了 1 次」。实际项目中你很可能需要更灵活的校验。
verify(exactly = 0)—— 确认一次都没调过verify(atLeast = 1, atMost = 3)—— 调了 1 到 3 次verify { ... }默认 exactly = 1verifyOrder { ... }—— 严格按照调用顺序
我记得有一个线上 bug 就是因为某个缓存方法被重复调了两次,导致性能问题。当时测试只写了 verify { cache.get(key) },默认是 exactly=1,但实际被测代码里因为并发跑了两次。后来我把验证改成 verify(exactly = 1) 加上 confirmVerified,立刻就发现了问题。你看,一个小小的细节就能拦住大坑。
confirmVerified 到底是什么?
很多人会忽略它。简单说:它检查当前 Mock 对象上 所有已发生的调用 是否都已经被你验证过了。如果有一个调用了但你没写 verify,它会抛出异常。
⚠️ 重要提醒:不要每个测试都无脑加 confirmVerified。它只在你想要“严格模式下”——不希望有任何未被验证的调用时有用。如果 Mock 上有些调用是框架内部产生的,反而会误报。我建议一开始先用它,等测试稳定了再决定是否保留。
避坑指南:创建 Mock 时别掉进这些坑
讲几个我亲自踩过的坑。
- 忘了 every 就直接 verify:MockK 默认不记录所有调用,你不写 every 就无法验证。写
every { mock.method() } returns ...是前提。我记得有一次重构时,把 every 删了只留 verify,结果测试跑完一言不发通过了——因为 MockK 对未定义的调用会返回默认值,但verify依然能通过,然而实际行为根本不对。 - 变量作用域问题:类属性的 Mock 对象如果在
@BeforeEach里重新创建,记得把every放在@BeforeEach之后,否则重复定义会报MockKException: Missing calls inside verify。 - 误用 spyk:
spyk部分 Mock 很好用,但如果你忘记对某个方法写every,它会直接调用真实方法。我在一个测试里用 spyk 模拟了一个 Repository,结果没写 every 的查询方法真的跑去数据库了,测试突然变慢,查了半天才发现是 spyk 的锅。
💡 最佳实践:写测试时先假设所有外部依赖全部 Mock。用 mockk<T>(),配合 relaxed = true(下一章会讲),可以避免很多遗漏。但记住,relaxed Mock 并不会帮你验证行为,只能帮你快速让测试通过。
本章小结
创建 Mock 对象和基本验证,是 MockK 测试的第一道门槛。你掌握 mockk<T>()、every、verify、confirmVerified 这四个元素,就可以写绝大多数场景下的回归测试了。别觉得简单,我见过太多复杂项目里,测试团队因为没搞懂这几个基础点,写出来的 Mock 测试要么变成弱化版的集成测试,要么完全不验证任何行为。