MockK:它是什么、关键概念和语法

作者: IT Sectr 发布日期: 2026-04-08 阅读时间: 8 分钟

MockK — 是一个 Kotlin-first 框架,用于创建 mock 对象,专门为 Kotlin 生态系统设计,考虑到其语言特点:协程、扩展函数、data class 和 sealed class。与从 Java 移植到 Kotlin 的 Mockito 不同,MockK 从一开始就是为 Kotlin 语法设计的,不需要额外的插件来处理 final 类。据 MockK.io 的数据,该库被超过 40% 的 Kotlin 项目用于单元测试。

核心要点

  • MockK — 面向 Kotlin 的 mock 库,支持协程和语言特性。
  • mockk() — 创建 mock 对象的主要方法,类似于 Mockito.mock()。
  • every { } — 用于以声明式风格配置 mock 行为(stubbing)的块。
  • coEvery / coVerify — 用于处理协程的 suspend 函数的特殊构造。
  • Relaxed mock — 无需显式 stubbing 即可返回默认值的 mock。

什么是 MockK?

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 如何工作

机制 MockK 基于通过库 ByteBuddy 进行的字节码插档(与 Mockito 相同),但将其包装在一个友好的 Kotlin DSL 中。代替 when().thenReturn() 链,MockK 使用 every { } 和 coEvery { } lambda 块,这些块看起来像语言的自然扩展。在内部,MockK 截获 lambda 内的调用,通过反射分析方法和参数,并将其与记录的 stubbing 规则进行匹配。

MockK 基础语法

every { mock.method() } returns value 块的含义是“每次调用该方法时,返回该值”。这种声明式语法更接近 Kotlin 风格,消除了 when() 中参数顺序的混淆。凭借 Kotlin 的 reified generics,mock 的类型可以自动推导,无需显式指定类。

kotlin
val repository = mockk<UserRepository>()

// Stubbing:每次调用 findById(1) 返回用户
every { repository.findById(1) } returns User("Alice")

// 调用和验证
val result = repository.findById(1)
assertEquals("Alice", result.name)

Relaxed mock:更少的模板代码

与 Mockito 不同,Mockito 中每个方法都需要显式配置,而 MockK 支持 relaxed mock——一种为任何方法返回“合理”默认值的 mock:List 返回空列表,Int 返回 0,String 返回空字符串。这大幅减少了准备代码的量。

kotlin
// Relaxed mock — 所有方法返回默认值
val api = mockk<ApiService>(relaxed = true)

// 不需要 stubbing — 将返回空列表
println(api.getUsers()) // []

创建 mock 和松弛 mock

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。

Stubbing:使用 every 块配置行为

Every 块 — 是 MockK 中 stubbing 的核心构造。在 lambda 内部,描述了带具体参数的方法调用,然后通过 returns 返回值,通过 throws 抛出异常,或通过 answers 计算答案。

Stubbing 的不同方式

MockK 支持测试所需的所有场景:返回值、抛出异常、根据参数计算答案、按顺序多次答复(调用顺序)。

kotlin
// 返回值
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 和 coVerify

Verify 在 MockK 中意义上与 Mockito.verify() 类似,但使用了 Kotlin DSL:verify { mock.method() }。对于 suspend 函数,使用 coVerify { mock.suspendMethod() },它能正确处理协程且不需要特殊运行器。

检查调用次数

MockK 支持与 Mockito 相同的修饰符:exactly(1)、atLeast(2)、atMost(5)、wasNot(Called)。语法极简——修饰符作为第一个参数传递给 verify { }。

kotlin
// 验证:方法被精确调用了1次
verify(exactly = 1) { repo.save(any()) }

// 检查调用顺序
verifySequence {
    repo.save(any())
    repo.flush()
}

// 针对 suspend 函数的 coVerify
coVerify { api.fetchUsers() }

Slot:捕获参数

为了验证参数,使用 slot() — ArgumentCaptor 的等价物。Slot 在调用之前声明,传递给 every 或 verify,在测试执行后包含捕获的值。

kotlin
val userSlot = slot<User>()

verify { repo.save(capture(userSlot)) }

assertEquals("Alice", userSlot.captured.name)

MockK 注解和 JUnit 集成

MockK 提供 @MockK@RelaxedMockK 注解,用于通过 JUnit 5 中的初始化来创建 mock。扩展 MockKExtension 在每次测试前自动创建 mock 并在测试后清理——类似 MockitoExtension,但支持 relaxed 模式。

使用 MockKExtension 的示例

@InjectMockKs 注解(或替代方案 @MockK 与显式对象创建)将 mock 注入到被测对象中。这减少了模板代码,使测试代码更干净。

kotlin
@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 vs Mockito:为 Kotlin 选择什么

选择 MockK 和 Mockito 之间取决于团队组成和项目类型。Mockito 拥有更大的生态系统、更多示例和集成,但 MockK 提供更干净的 Kotlin 语法和对语言特性的原生支持。对于新的 Kotlin 项目,建议使用 MockK 作为更符合语言习惯的解决方案。

标准MockKMockito
语法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 与普通 mock 在 MockK 中有什么区别?

Relaxed mock为所有未 stub 的方法返回默认值(空列表、0、null),不抛出异常。普通(strict) mock 需要每个方法的显式 stubbing——否则测试失败。Relaxed mock 适合快速测试,strict 适合可靠的测试。

如何在 MockK 中 mock 扩展函数?

MockK 通过 mockkStatic() 支持 mock 扩展函数。这是可能的,因为 Kotlin 中的扩展函数是静态方法,第一个参数是接收者。每个扩展函数都需要指定它所在的类。

MockK 可以与 Kotlin Multiplatform 一起使用吗?

可以,MockK 支持共同代码的 Kotlin Multiplatform (KMP)。在 JVM、Native 和 JS 平台上均可使用共同 API:mockk()、every、verify。这使得 MockK 成为 KMP 项目的唯一选择,因为 Mockito 在这些平台上无法工作。

如何检查 MockK 中的调用顺序?

使用 verifySequence { }——一个块,其中调用严格按照预期顺序指定。如果实际顺序不同,verifySequence 将抛出异常,指明第一个不匹配的调用。

可以在同一个项目中同时使用 MockK 和 Mockito 吗?

可以,技术上是可行的,但不推荐。冲突可能发生在字节码插档层面(ByteBuddy vs mockito-inline)。如果项目已经使用 Mockito,可以通过模块隔离逐步迁移到 MockK。

总结

  • MockK — 一个 Kotlin-first mock 库,提供原生语言支持。
  • every { } — 用于配置 mock 行为的声明式 DSL。
  • coEvery / coVerify — 无需额外依赖即可支持协程 suspend 函数。
  • Relaxed mock — 带有默认值的 mock,减少模板代码。
  • @MockK / @InjectMockKs — 用于在 JUnit 5 中自动创建 mock 的注解。
  • MockK vs Mockito — 对于纯 Kotlin 和 KMP 项目,MockK 更优。
  • verifySequence — 检查方法调用的严格顺序。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读