关于 mocking-with-mockito
此技能指南介绍在 Kotlin Android 测试中使用 Mockito 和 mockito-kotlin,包括依赖配置、模拟生命周期及 Kotlin DSL。适用于编写测试的 Android 开发者,并说明最终类模拟、参数匹配器和协程测试的注意事项。
主要功能
- 01配置 Mockito 与 mockito-kotlin
- 02选择模拟对象生命周期管理方式
- 03使用 Kotlin DSL 创建模拟和捕获参数
- 04正确组合参数匹配器
- 05配置最终类模拟
- 06启用严格桩检查
适用场景
- 01为 Kotlin Android 单元测试创建模拟对象
- 02解决最终类无法模拟的问题
- 03排查参数匹配器异常或未使用的桩
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill mocking-with-mockito安装后在 Claude Code 中输入/mocking-with-mockito确认已加载。
详细介绍
使用 Mockito 进行模拟 — androidx 原生模拟技术栈
Mockito 是 androidx 自己使用的模拟框架——在 AOSP 检出代码中,有 400 多个测试文件导入了 org.mockito,而 io.mockk 的命中数为 0。希望“与 Google 的做法保持一致”的 Android 新代码会选择 Mockito + mockito-kotlin Kotlin DSL。本技能介绍精确的依赖矩阵、三种设置方式、Kotlin DSL 的 API,以及 Kotlin final 类所需的 inline mock maker 配置。
何时使用本技能
- 用户正在 Kotlin Android 模块中开始编写新的测试套件,并询问“应该使用什么模拟库”。
- 用户在使用 Mockito 4.x 时遇到
Cannot mock/spy class … final class,需要配置mock-maker-inline。 - 用户因混用原始值与
any()/eq()而遇到InvalidUseOfMatchersException。 - 用户正在处理 Kotlin 中与
when关键字的冲突(`when`(mock.foo())),并希望使用wheneverDSL。 - 用户希望使用
argumentCaptor<T>()的firstValue/lastValue/allValues,而不是ArgumentCaptor.forClass(...)。 - 用户正在从
MockitoAnnotations.initMocks(this)迁移到openMocks(this)或MockitoJUnit.rule()方式。 - 用户正在 API < 28 的设备上运行插桩测试,并需要模拟
final类。
何时不要使用本技能
- 用户需要为挂起函数使用
coEvery/coVerify、通过mockkObject深度模拟单例,或通过mockkStatic模拟静态方法——请使用../mocking-with-mockk/SKILL.md。 - 用户正在设计层面比较 fake 和 mock——请使用
../../../fundamentals/doubles/picking-test-doubles/SKILL.md。根据 /test-doubles,对于行为较多的协作者,优先使用 fake 而不是 mock。 - Runner / Gradle 矩阵尚未配置——请从
../../runner/configuring-junit4-on-android/SKILL.md开始。 - 用户需要支持
runTest的协程测试——请参阅../../coroutines/testing-coroutines-with-runtest/SKILL.md。
前置条件
- 已完成
../../runner/configuring-junit4-on-android/SKILL.md中的基础测试配置(@RunWith(AndroidJUnit4::class)、androidx.test.ext:junit:1.3.0、JUnit 4.13.2)。 - 使用 JDK 11+ 的 Kotlin 模块(Mockito 5.x 将 JDK 最低版本要求提升至 Java 11)。
- 如果被测代码会访问
Dispatchers.Main,则已安装MainDispatcherRule——请参阅 runner 技能。
工作流
- 1. 添加 Mockito 和 mockito-kotlin 的依赖坐标。 推荐以 Mockito 5.x 作为基线,因为它默认启用 inline mock maker——模拟 Kotlin final 类无需插件文件或替换为
mockito-inline:
dependencies {
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
// Instrumented final-class mocks (API 28+ only — see step 7)
androidTestImplementation("org.mockito:mockito-android:5.14.2")
androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
}
mockito-kotlin 并非 fork——它只是一个直接委托给 mockito-core 的轻量 DSL 辅助层。两者都必须位于类路径中。
- 2. 选择 mock 生命周期管理方式。 有三种惯用选项。新代码优先使用 rule;只有在测试类无法声明
@RunWith时才使用openMocks。
// (a) JUnit4 rule — preferred. No runner ownership, plays nicely with @RunWith(AndroidJUnit4::class).
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
// (b) Manual openMocks — works everywhere.
private lateinit var closeable: AutoCloseable
@Before fun setUp() { closeable = MockitoAnnotations.openMocks(this) }
@After fun tearDown() { closeable.close() }
// (c) JUnit5 extension (only if the project is on JUnit 5 via the Vintage engine).
@ExtendWith(MockitoExtension::class)
已弃用的 MockitoAnnotations.initMocks(this) 返回 void,并会泄漏 inline mock 资源。始终使用 openMocks(this),并在 @After 中关闭其返回的 AutoCloseable。
- 3. 声明 mock,并使用 mockito-kotlin DSL 为其设置 stub。 适用于 Kotlin 的入口位于
org.mockito.kotlin.*下:
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
class UserViewModelTest {
@get:Rule val mockitoRule = MockitoJUnit.rule()
@get:Rule val mainDispatcherRule = MainDispatcherRule()
private val repo: UserRepository = mock()
private val analytics: Analytics = mock {
on { isEnabled() } doReturn true
}
@Test fun loadsUserAndLogs() = runTest {
whenever(repo.findById(7L)) doReturn User("Jane")
val vm = UserViewModel(repo, analytics)
vm.load(7L)
advanceUntilIdle()
val captor = argumentCaptor<AnalyticsEvent>()
verify(analytics, atLeastOnce()).log(captor.capture())
assertThat(captor.lastValue.name).isEqualTo("user_loaded")
verify(repo).findById(eq(7L))
}
}
要点:
mock<T>()使用 reified 类型参数——无需编写T::class.java。mock { on { … } doReturn … }是一个构建器代码块——在创建时设置 stub,可用它替代一串whenever调用。whenever(...)替代原始 Mockito 的`when`(...)(由于when是 Kotlin 关键字,因此必须使用反引号)。argumentCaptor<T>()返回一个 captor,并提供firstValue/secondValue/lastValue/allValues属性——无需调用getValue()。org.mockito.kotlin.*中的any()、anyOrNull()、eq()在 Kotlin 中可安全处理 null(原始 Mockito 的any()会返回 Javanull,导致 Kotlin 非空参数崩溃)。- 4. 遵循
any/eq匹配器混用规则。 如果任何参数使用了匹配器(any()、eq()、argThat { … }),则所有参数都必须使用匹配器。将原始值与any()混用会引发InvalidUseOfMatchersException: Misplaced or misused argument matcher。用eq(...)包装原始值:
verify(repo).save(any(), eq("identifier")) // RIGHT — all args are matchers
verify(repo).save(any(), "identifier") // WRONG — InvalidUseOfMatchersException
- 5. 在任意挂起上下文中 stub 挂起函数。 Mockito 不提供一等的挂起支持。只有当调用点本身处于挂起上下文中时,编译器才接受
whenever(mock.suspendFn())。runTest { … }已经提供了该挂起上下文——在runTest { … }中用runBlocking { … }包裹 stub 会阻塞测试调度器,并可能在使用MainDispatcherRule时造成死锁。二者选其一,不要同时使用。 这是 Mockito 在易用性方面的主要不足;如果代码库大量使用挂起函数,请考虑改用../mocking-with-mockk/SKILL.md。
@Test fun loadsUser() = runTest {
whenever(repo.fetchUser(1)) doReturn User("Jane") // RIGHT — runTest IS a suspend context
val vm = UserViewModel(repo)
vm.load(1)
advanceUntilIdle()
verify(repo).fetchUser(1)
}
对于非挂起测试(不使用 runTest),请改用 runBlocking——但绝不要同时使用二者:
@Test fun loadsUser() = runBlocking {
whenever(repo.fetchUser(1)) doReturn User("Jane")
val result = UserService(repo).get(1)
assertThat(result.name).isEqualTo("Jane")
}
- 6. 对于 Mockito 4.x,通过资源文件安装 inline mock-maker。 Mockito 5.x 默认已启用它。对于较旧版本,在
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker创建一个仅包含一行内容的文件:
mock-maker-inline
这与 androidx 在 compose/material/material/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker、compose/foundation/foundation/.../mockito-extensions/... 和 room3/room3-runtime/.../mockito-extensions/... 中提供的内容完全相同。另一种方式是在 Gradle 中将 mockito-core 换成 mockito-inline(后者会传递依赖 mockito-core)。
inline 仍有限制:无法模拟 final 私有成员、equals()/hashCode()、String 和 Class。
- 7. 对 API < 28 的插桩测试,限制 final 类模拟。
mockito-android使用 dexmaker,而 dexmaker 仅支持在 API 28+ 上模拟 final 类。这是 dexmaker 的限制,而非 Mockito 版本的限制——升级到 Mockito 5.x 无法解除此限制;只有 Android API 28(P)引入了 dexmaker 所需的 JVMTI 钩子。androidx 的惯例是在测试类上添加@SdkSuppress(minSdkVersion = 28),并附上一行注释。来自compose/ui/ui/src/androidHostTest/kotlin/androidx/compose/ui/input/IndirectPointerEventWithInputDeviceMockTest.kt的真实示例:
import androidx.test.filters.SdkSuppress
@RunWith(MockitoJUnitRunner::class)
@SdkSuppress(minSdkVersion = 28) // Mocks for final classes can only be done on 28 and higher
class IndirectPointerEventWithInputDeviceMockTest { /* ... */ }
- 8. 为新的测试类应用
Strictness.STRICT_STUBS。 严格 stub 会将未使用的 stub 和参数不匹配作为失败报告,从而发现那些因实际上没有使用 stub 而“通过”的测试。通过规则配置:
@get:Rule val mockitoRule: MockitoRule =
MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)
- 9. 验证测试套件可以运行,并产生预期的交互。
./gradlew :<module>:testDebugUnitTest --tests com.example.UserViewModelTest。验证失败时会产生标准的 “Wanted but not invoked” / “Argument(s) are different” 输出——这些消息通常能帮助定位问题。
模式
模式:错误与正确 — Kotlin when 关键字冲突
// WRONG — raw Mockito from Kotlin
import org.mockito.Mockito.`when`
`when`(repo.fetch()).thenReturn(User("Jane"))
// WRONG because: `when` requires backtick-escaping in Kotlin (it is a reserved keyword),
// the import line is awkward, and any() returns Java null which crashes Kotlin
// non-nullable parameters. mockito-kotlin's whenever / null-safe matchers solve both.
// RIGHT — mockito-kotlin DSL
import org.mockito.kotlin.whenever
import org.mockito.kotlin.doReturn
whenever(repo.fetch()) doReturn User("Jane")
模式:错误与正确 — 参数捕获器过于冗长
// WRONG
import org.mockito.ArgumentCaptor
val captor = ArgumentCaptor.forClass(User::class.java)
verify(repo).save(captor.capture())
assertEquals("Jane", captor.value.name)
// WRONG because: ArgumentCaptor.forClass requires .java, captor.value collides with
// Kotlin's value class semantics for some types, and there's no firstValue/lastValue
// accessor for sequences of captures.
// RIGHT
import org.mockito.kotlin.argumentCaptor
val captor = argumentCaptor<User>()
verify(repo, atLeastOnce()).save(captor.capture())
assertEquals("Jane", captor.lastValue.name)
val all: List<User> = captor.allValues
模式:错误与正确 — Mockito 4.x 中模拟 final 类
// WRONG — Mockito 4.x without the inline plugin, mocking a Kotlin final class
val service: PaymentService = mock()
// WRONG because: Kotlin classes are final by default. The classic subclass mock-maker
// throws "Cannot mock/spy class com.example.PaymentService — final class". Either
// upgrade to Mockito 5.x (inline by default) or install the plugin file.
// RIGHT — src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
mock-maker-inline
// Mockito 5.x: nothing else to do; mock<PaymentService>() works out of the box.
模式:错误与正确 — 严格 stub 会捕获无效 stub
// WRONG — silent passing test
class MyTest {
@get:Rule val mockitoRule = MockitoJUnit.rule() // default lenient
@Test fun loadsUser() {
whenever(repo.findById(7L)) doReturn User("Jane")
val vm = UserViewModel(repo)
vm.load(8L) // wrong id — stub is never used
verify(repo).findById(any()) // passes because findById was called
}
}
// WRONG because: the test claims to verify behaviour for id=7 but actually exercises id=8.
// The stub is dead. STRICT_STUBS catches this with PotentialStubbingProblem.
// RIGHT
@get:Rule val mockitoRule: MockitoRule =
MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)
// Now PotentialStubbingProblem fires at runtime if the stub argument doesn't match
// what the SUT actually calls.
强制规则
- 必须为 Kotlin 测试同时添加
org.mockito:mockito-core:5.x和org.mockito.kotlin:mockito-kotlin:5.x。在任何现代 Kotlin 代码库中,DSL 工件都是必需的。 - 必须使用
org.mockito.kotlin.*中的whenever(...),而不是org.mockito.Mockito中的`when`(...)。转义关键字的写法是一种代码异味。 - 必须在 Kotlin 测试中使用
org.mockito.kotlin的any()/anyOrNull()/eq()。原生 Mockito 的any()会返回 Javanull,并在运行时导致 Kotlin 非空参数崩溃。 - 必须在挂起上下文中为挂起函数配置桩——使用
runTest { whenever(...) doReturn ... }(首选,与虚拟时间集成),或在非runTest的设置中使用runBlocking { ... }。不得在runTest中嵌套runBlocking——它会阻塞测试调度器,并可能在MainDispatcherRule下导致死锁。Mockito 没有原生的挂起函数桩配置接口;任一挂起上下文都足够。 - 必须使用
MockitoAnnotations.openMocks(this),并在@After中关闭其返回的AutoCloseable。不得使用已弃用的MockitoAnnotations.initMocks(this)。 - 必须为任何模拟 Kotlin final 类的 5.x 之前的项目提供
mockito-extensions/org.mockito.plugins.MockMaker,其字面内容为mock-maker-inline。Mockito 5.x 默认启用 inline,无需额外配置。 - 必须为通过
mockito-android以androidTestImplementation运行且模拟 final 类的测试类添加@SdkSuppress(minSdkVersion = 28)注解。 - 必须在单个
verify/whenever调用中保持匹配器一致:要么全部使用匹配器,要么全部不使用。要混用时,请用eq(...)包装原始值。 - 必须优先为具有行为的协作者使用 fake 而非 mock(参见 developer.android.com/training/testing/fundamentals/test-doubles:“fake ... are preferred”(优先使用 fake))。使用 Mockito 验证交互,而不是重新实现行为。
- 不得在同一个文件中混用
Mockito.mock(...)和org.mockito.kotlin.mock<T>()——选用一种并保持一致。优先使用 Kotlin DSL。 - **首选:**应用
Strictness.STRICT_STUBS,以捕获未使用的桩和参数不匹配。宽松模式会掩盖错误。
验证
-
./gradlew :<module>:testDebugUnitTest --tests <YourTestClass>执行成功。 -
grep -rn "org.mockito.Mockito.\when`" src/test src/androidTest无匹配项(改用whenever`)。 -
grep -rn "MockitoAnnotations.initMocks" src/test src/androidTest无匹配项(改用openMocks)。 - 新代码中,
grep -rn "ArgumentCaptor.forClass" src/test src/androidTest无匹配项(改用argumentCaptor<T>())。 - 如果项目使用 Mockito 4.x 且模拟 Kotlin final 类,则文件
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker必须存在,且字面内容为mock-maker-inline。 - 已应用
MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)(或 JUnit5 注解@MockitoSettings(strictness = Strictness.STRICT_STUBS))。 -
Strictness.STRICT_STUBS下未报告未使用的桩。 - final 类的仪器化测试带有
@SdkSuppress(minSdkVersion = 28)。
参考资料
- Mockito 参考文档:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
- mockito-kotlin:https://github.com/mockito/mockito-kotlin
- Mockito 5 发行说明:https://github.com/mockito/mockito/releases/tag/v5.0.0
- Android Developers — 本地测试:https://developer.android.com/training/testing/local-tests
- Android Developers — 测试替身:https://developer.android.com/training/testing/fundamentals/test-doubles
androidx/car/app/app/src/test/java/androidx/car/app/serialization/ListDelegateTest.kt— mockito-kotlin DSL 的标准示例(mock<T>()、argumentCaptor<T>()、verify(... atLeastOnce())、lastValue)。androidx/compose/ui/ui/src/androidHostTest/kotlin/androidx/compose/ui/input/IndirectPointerEventWithInputDeviceMockTest.kt—@RunWith(MockitoJUnitRunner::class)+@SdkSuppress(minSdkVersion = 28)+ 原始`when`(旧式写法)。androidx/compose/material/material/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— 文件内容为字面值mock-maker-inline。androidx/compose/foundation/foundation/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— 同一插件文件约定。androidx/room3/room3-runtime/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— 同一插件文件约定。