MockK — 是一个 Kotlin-first 框架,用于创建 mock 对象,专门为 Kotlin 生态系统设计,考虑到其语言特点:协程、扩展函数、data class 和 sealed class。与从 Java 移植到 Kotlin 的 Mockito 不同,MockK 从一开始就是为 Kotlin 语法设计的,不需要额外的插件来处理 final 类。据 MockK.io 的数据,该库被超过 40% 的 Kotlin 项目用于单元测试。
核心要点
MockK — 是一个用于创建 mock 对象的库,用 Kotlin 编写并针对其语法进行了优化。它解决与 Mockito 相同的任务——将被测试代码与依赖隔离——但它使用了 Kotlin 特定的构造: lambda、DSL、reified generics 和 suspend 函数。
MockK 与移植方案相比的主要优势是 对 Kotlin 的原生支持。在 Mockito 中,mock final 类需要 opt-in(mockito-inline),静态方法需要 mockStatic。MockK 默认支持这些,因为 Kotlin 类默认是 final 的,而绕过这个限制的能力已内置在库的架构中。
版本 1.13.12 (2024) — 稳定版本,支持 Kotlin 2.0、K2 编译器和多平台项目 (KMP)。MockK 还可以与 Kotlin/Native 和 Kotlin/JS 一起使用,这使它成为 KMP 项目的唯一选择,而 Mockito 和 EasyMock 均不可用。
MockK 是考虑到 Kotlin 的特殊性设计的,并利用语言能力——reified generics、带 lambda 的 DSL、inline 函数——提供简洁且类型安全的 API,不会损失性能。
机制 MockK 基于通过库 ByteBuddy 进行的字节码插档(与 Mockito 相同),但将其包装在一个友好的 Kotlin DSL 中。代替 when().thenReturn() 链,MockK 使用 every { } 和 coEvery { } lambda 块,这些块看起来像语言的自然扩展。在内部,MockK 截获 lambda 内的调用,通过反射分析方法和参数,并将其与记录的 stubbing 规则进行匹配。
every { mock.method() } returns value 块的含义是“每次调用该方法时,返回该值”。这种声明式语法更接近 Kotlin 风格,消除了 when() 中参数顺序的混淆。凭借 Kotlin 的 reified generics,mock 的类型可以自动推导,无需显式指定类。
val repository = mockk<UserRepository>()
// Stubbing:每次调用 findById(1) 返回用户
every { repository.findById(1) } returns User("Alice")
// 调用和验证
val result = repository.findById(1)
assertEquals("Alice", result.name)
与 Mockito 不同,Mockito 中每个方法都需要显式配置,而 MockK 支持 relaxed mock——一种为任何方法返回“合理”默认值的 mock:List 返回空列表,Int 返回 0,String 返回空字符串。这大幅减少了准备代码的量。
// Relaxed mock — 所有方法返回默认值
val api = mockk<ApiService>(relaxed = true)
// 不需要 stubbing — 将返回空列表
println(api.getUsers()) // []
MockK 提供了多种创建 mock 对象的方法:mockk<T>() 用于严格 mock(每个方法都必须显式配置),mockk<T>(relaxed = true) 用于松弛 mock,以及 spyk(obj) 用于在真实对象上创建 spy。
| 函数 | 类型 | 无 stubbing 时的行为 |
|---|---|---|
| mockk() | 严格 mock | 调用未 stub 的方法时抛出异常 |
| mockk(relaxed = true) | 松弛 mock | 返回默认值 |
| spyk() | Spy | 如果没有配置 stub,则调用真实方法 |
| slot() | Argument Captor | 捕获参数以供验证 |
在严格和松弛 mock 之间的选择取决于上下文。严格 mock 确保测试不使用行为未定义的方法——这提高了可靠性。松弛 mock 适合快速原型测试,其中并非所有依赖都很重要。在实践中,建议从严格 mock 开始,只在 stubbing 占据的行数多于测试本身时才转向 relaxed。
Every 块 — 是 MockK 中 stubbing 的核心构造。在 lambda 内部,描述了带具体参数的方法调用,然后通过 returns 返回值,通过 throws 抛出异常,或通过 answers 计算答案。
MockK 支持测试所需的所有场景:返回值、抛出异常、根据参数计算答案、按顺序多次答复(调用顺序)。
// 返回值
every { repo.findById(1) } returns User("Alice")
// 抛出异常
every { repo.findById(999) } throws NotFoundException()
// 动态答复
every { repo.save(any()) } answers {
val user = firstArg<User>()
user.copy(id = 42)
}
// 答复顺序
every { repo.findAll() } returnsMany listOf(
listOf(User("Alice")),
listOf(User("Bob")),
emptyList()
)
Verify 在 MockK 中意义上与 Mockito.verify() 类似,但使用了 Kotlin DSL:verify { mock.method() }。对于 suspend 函数,使用 coVerify { mock.suspendMethod() },它能正确处理协程且不需要特殊运行器。
MockK 支持与 Mockito 相同的修饰符:exactly(1)、atLeast(2)、atMost(5)、wasNot(Called)。语法极简——修饰符作为第一个参数传递给 verify { }。
// 验证:方法被精确调用了1次
verify(exactly = 1) { repo.save(any()) }
// 检查调用顺序
verifySequence {
repo.save(any())
repo.flush()
}
// 针对 suspend 函数的 coVerify
coVerify { api.fetchUsers() }
为了验证参数,使用 slot() — ArgumentCaptor 的等价物。Slot 在调用之前声明,传递给 every 或 verify,在测试执行后包含捕获的值。
val userSlot = slot<User>()
verify { repo.save(capture(userSlot)) }
assertEquals("Alice", userSlot.captured.name)
MockK 提供 @MockK 和 @RelaxedMockK 注解,用于通过 JUnit 5 中的初始化来创建 mock。扩展 MockKExtension 在每次测试前自动创建 mock 并在测试后清理——类似 MockitoExtension,但支持 relaxed 模式。
@InjectMockKs 注解(或替代方案 @MockK 与显式对象创建)将 mock 注入到被测对象中。这减少了模板代码,使测试代码更干净。
@ExtendWith(MockKExtension::class)
class UserServiceTest {
@MockK
lateinit var repository: UserRepository
@InjectMockKs
lateinit var service: UserService
@Test
fun `getUser returns user from repository`() {
every { repository.findById(1) } returns User("Alice")
assertEquals("Alice", service.getUser(1)?.name)
}
}
选择 MockK 和 Mockito 之间取决于团队组成和项目类型。Mockito 拥有更大的生态系统、更多示例和集成,但 MockK 提供更干净的 Kotlin 语法和对语言特性的原生支持。对于新的 Kotlin 项目,建议使用 MockK 作为更符合语言习惯的解决方案。
| 标准 | MockK | Mockito |
|---|---|---|
| 语法 | Kotlin DSL (every, verify) | Java 风格 (when, thenReturn) |
| 协程 | coEvery, coVerify (原生) | 需要额外库 |
| Final class | 默认支持 | 需要 mockito-inline |
| KMP | 支持 | 不支持 |
| Relaxed mock | 内置 | 无等价物 |
| 流行度 | 在 Kotlin 社区中上升 | 在 Java 和混合项目中占主导地位 |
对于 纯 Kotlin 项目(无 Java 类),MockK 更优:更少的模板代码、原生协程支持、无 final 类问题。对于混合项目或具有 Java 背景的团队,Mockito 仍然是可行的选择——两个库可以通过不同模块在同一项目中使用。从 Mockito 迁移到 MockK 时,只需将 @Mock 注解替换为 @MockK,并将 when().thenReturn() 块重写为 every { } 格式。
常见问题
Relaxed mock为所有未 stub 的方法返回默认值(空列表、0、null),不抛出异常。普通(strict) mock 需要每个方法的显式 stubbing——否则测试失败。Relaxed mock 适合快速测试,strict 适合可靠的测试。
MockK 通过 mockkStatic() 支持 mock 扩展函数。这是可能的,因为 Kotlin 中的扩展函数是静态方法,第一个参数是接收者。每个扩展函数都需要指定它所在的类。
可以,MockK 支持共同代码的 Kotlin Multiplatform (KMP)。在 JVM、Native 和 JS 平台上均可使用共同 API:mockk()、every、verify。这使得 MockK 成为 KMP 项目的唯一选择,因为 Mockito 在这些平台上无法工作。
使用 verifySequence { }——一个块,其中调用严格按照预期顺序指定。如果实际顺序不同,verifySequence 将抛出异常,指明第一个不匹配的调用。
可以,技术上是可行的,但不推荐。冲突可能发生在字节码插档层面(ByteBuddy vs mockito-inline)。如果项目已经使用 Mockito,可以通过模块隔离逐步迁移到 MockK。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。