3. MockK 基本用法:mock、spy、relaxed mock、slot、capture、every/coEvery、verify/coVerify

本章围绕 MockK 协程测试核心构造展开,覆盖 mock 对象创建、spy 部分模拟、relaxed mock 自动桩、参数捕获(slot/capture)、行为定义(every/coEvery)和验证(verify/coVerify)。掌握这些基础 API 是后续协程挂起函数测试的基石。

3.1 核心概念对比

构造 用途 是否默认返回默认值 适用场景
mockk<T>() 创建完整 mock 对象 否(需显式定义行为) 全模拟、隔离被测类
spyk(obj) 创建 spy 对象(包装真实实例) 是(未桩方法委托给真实对象) 部分模拟、保留真实逻辑
mockk<T>(relaxed = true) 创建 relaxed mock 是(返回类型默认值) 快速 mock、无需逐一定义返回值
slot<T>() 参数捕获槽 — 捕获调用时传入的具体参数
capture(slot) 在 every/verify 中捕获参数 — 配合 slot 验证参数内容

3.2 创建 mock 与行为定义

使用 mockk() 创建普通 mock,必须通过 every 或 coEvery 定义返回值。

interface UserRepository {
    suspend fun findById(id: Int): User?
    fun getName(id: Int): String
}

val repo = mockk<UserRepository>()
every { repo.getName(1) } returns "Alice"

coEvery { repo.findById(1) } returns User(1, "Alice")

println(repo.getName(1))       // Alice
runBlocking { println(repo.findById(1)) }  // User(1, "Alice")

注意:coEvery 用于挂起函数,every 用于普通函数。若在协程环境中使用 every 定义挂起函数,MockK 会抛出异常。

3.3 relaxed mock:自动桩

设置 relaxed = true 后,未定义行为的调用返回“类型默认值”(数字 0、布尔 false、字符串空、集合空、可空类型 null)。

val relaxedRepo = mockk<UserRepository>(relaxed = true)

// 无需 every 即可调用,返回默认值
println(relaxedRepo.getName(99))          // ""
runBlocking { println(relaxedRepo.findById(99)) } // null
注意: relaxed mock 不会验证参数匹配,所有未捕获的调用都返回默认值。如果希望验证参数,请配合 slot 和 capture 使用。

3.4 spy:保留真实对象的部分逻辑

spyk 将真实实例包装为 spy,未桩方法委托给真实实现;已桩方法覆盖原行为。

class UserService {
    fun getDisplayName(id: Int): String {
        return "User:$id"
    }
}

val service = spyk(UserService())
every { service.getDisplayName(1) } returns "Admin"
println(service.getDisplayName(1))   // Admin(桩覆盖)
println(service.getDisplayName(2))   // User:2(委托真实)
提示: spy 适合处理复杂依赖的遗留代码,但应优先使用 mock 以保持测试纯粹性。

3.5 参数捕获:slot 与 capture

使用 slot 创建捕获槽,在 every 或 verify 中通过 capture 捕获实际入参。

val slotName = slot<Int>()
every { repo.getName(capture(slotName)) } answers {
    "captured: ${slotName.captured}"
}

repo.getName(42)
println(slotName.captured)  // 42

同样可用于 verify 阶段的参数验证:

val captor = slot<Int>()
verify { repo.getName(capture(captor)) }
assertEquals(42, captor.captured)

3.6 every / coEvery 行为定义

函数 作用 返回值语法 协程支持
every { mock.method() } 定义普通函数返回值 returns value 或 answers { … } 否(仅非挂起函数)
coEvery { mock.suspendMethod() } 定义挂起函数返回值 returns value 或 coAnswers { … } 是(协程上下文)
every { mock.method() } just Runs 定义 void 函数(Unit 返回) just Runs 否
coEvery { repo.findById(any()) } returns User(0, "default")
coEvery { repo.findById(gt(100)) } coAnswers {
    User(call.args[0] as Int, "large id")
}

// 协程 answers 块使用 coAnswers
coEvery { repo.findById(any()) } coAnswers {
    val id = call.args[0] as Int
    if (id < 0) null else User(id, "User$id")
}

3.7 verify / coVerify 验证交互

验证函数是否被调用、调用次数、参数匹配。

// 普通函数验证
verify(exactly = 1) { repo.getName(1) }

// 挂起函数验证
coVerify(exactly = 1) { repo.findById(1) }

// 验证从未被调用
verify(inverse = true) { repo.getName(99) }

// 验证至少一次
verify(atLeast = 1) { repo.getName(any()) }

// 验证顺序
verifyOrder {
    repo.getName(1)
    repo.getName(2)
}
关键规则:
  • coVerify 必须放在 runTest 或 runBlocking 块内。
  • 默认验证至少一次(atLeast = 1),显式使用 exactly 或 atMost 控制次数。
  • verifyAll 验证所有已定义的调用是否发生过。

3.8 核心流程:从 mock 创建到验证

MockK 基本用法流程 mockk() / spyk() relaxed mock slot + capture every / coEvery answers / coAnswers capture(slot) 测试代码调用 mock verify / coVerify ⚠️ 常见错误 • 混用 every 定义挂起函数 • 忘记使用 coVerify 验证挂起函数 • slot 未在 capture 前 clear

3.9 常见错误与调试

错误 原因 修复
every { mock.suspendFun() } 编译报错 every 不支持挂起函数 替换为 coEvery
verify 在 runTest 外无法验证 协程作用域不匹配 将 verify 放在 runTest/runBlocking 块内
slot.captured 为 null 未调用 capture(slot) 或未执行调用 检查 every 或 verify 中是否使用了 capture
relaxed mock 返回空集合而非预期值 未定义行为导致返回默认空集合 使用 every 显式定义特定参数返回值

3.10 性能与最佳实践

  • 优先 relaxed mock:对于只关心交互次数而不需精确返回值的测试,使用 relaxed mock 减少样板代码。
  • 限制 spy 使用:spy 会执行真实代码,可能引入副作用,仅在模拟遗留代码时使用。
  • slot 复用前清空:多次测试共用 slot 实例时,每次用前调用 slot.clear() 避免残留值。
  • coEvery 块中避免阻塞:coAnswers 内部应使用非阻塞代码,否则影响协程性能。
  • 使用 any() 匹配器:当参数不重要时用 any() 减少桩定义条数。
本节要点速查:
  1. mockk 创建 mock,spyk 包装真实对象。
  2. every / coEvery 定义普通/挂起函数行为。
  3. slot + capture 捕获参数用于断言。
  4. verify / coVerify 验证交互次数与参数。
  5. relaxed mock 自动提供默认值,减少样板代码。