mcpField
M
mocking-with-mockito

mocking-with-mockito

github.com/skydoves测试与质量33964 次安装22 周前更新
代码仓库

关于 mocking-with-mockito

此技能指南介绍在 Kotlin Android 测试中使用 Mockito 和 mockito-kotlin,包括依赖配置、模拟生命周期及 Kotlin DSL。适用于编写测试的 Android 开发者,并说明最终类模拟、参数匹配器和协程测试的注意事项。

主要功能

  1. 01配置 Mockito 与 mockito-kotlin
  2. 02选择模拟对象生命周期管理方式
  3. 03使用 Kotlin DSL 创建模拟和捕获参数
  4. 04正确组合参数匹配器
  5. 05配置最终类模拟
  6. 06启用严格桩检查

适用场景

  1. 01为 Kotlin Android 单元测试创建模拟对象
  2. 02解决最终类无法模拟的问题
  3. 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())),并希望使用 whenever DSL。
  • 用户希望使用 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() 会返回 Java null,导致 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() 会返回 Java null,并在运行时导致 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)。

参考资料

评论

支持 Markdown