第一个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 = 1
  • verifyOrder { ... } —— 严格按照调用顺序

我记得有一个线上 bug 就是因为某个缓存方法被重复调了两次,导致性能问题。当时测试只写了 verify { cache.get(key) },默认是 exactly=1,但实际被测代码里因为并发跑了两次。后来我把验证改成 verify(exactly = 1) 加上 confirmVerified,立刻就发现了问题。你看,一个小小的细节就能拦住大坑。

confirmVerified 到底是什么?

很多人会忽略它。简单说:它检查当前 Mock 对象上 所有已发生的调用 是否都已经被你验证过了。如果有一个调用了但你没写 verify,它会抛出异常。

⚠️ 重要提醒:不要每个测试都无脑加 confirmVerified。它只在你想要“严格模式下”——不希望有任何未被验证的调用时有用。如果 Mock 上有些调用是框架内部产生的,反而会误报。我建议一开始先用它,等测试稳定了再决定是否保留。

MockK 测试核心流程:创建 Mock + 基本验证 创建 Mock 对象 mockk<T>() 通用方式 mockkClass(T::class) spyk() 部分 Mock 基本验证 verify(exactly=1) verify(atLeast, atMost) verifyOrder 顺序验证 confirmVerified 额外检查 原则:先 every 定义行为,再执行被测代码,最后 verify/confirmVerified 做断言

避坑指南:创建 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 测试要么变成弱化版的集成测试,要么完全不验证任何行为。