Mock框架选择与配置:Mockk与Mockito在Compose中的使用、依赖配置

好,我们直接进入本章最核心的实操环节。很多人一上来就问:Compose 里到底用 Mockk 还是 Mockito?我个人的回答是——都可以,但要看场景。这两个框架在 Android 圈子里都算老熟人了,但到了 Compose 这个新世界,它们的一些特性差异会被放大。

两个框架的核心差异

先上一张对比表,帮你快速建立认知框架:

特性MockkMockito
Kotlin 原生支持✅ 一等公民,支持协程、扩展函数⚠️ 需要额外配置 mockito-kotlin
Compose 测试支持✅ 有专门的 compose-mockk 扩展库✅ 通过 mockito-android 支持
私有方法/静态方法✅ 内置支持(@RelaxedMockK)❌ 需 PowerMock(不推荐)
协程 Mock✅ coEvery / coVerify 原生⚠️ 需手动处理 suspend 函数
学习曲线中等(语法糖较多)较低(传统的 when/thenReturn)
社区活跃度📈 近年上升明显📊 老牌稳定,生态完整

你可能会问:为什么 Mockk 在 Compose 里越来越流行?说白了,Kotlin 的很多语法糖在 Mockito 里模拟起来很痛苦。比如 sealed class、operator 重载、协程 suspend……我在一个 Compose 项目里用 Mockito 去 mock 一个 ViewModel 的 StateFlow,硬是配了半天才跑通。换成 Mockk 之后,一行 coEvery { viewModel.uiState } returns MutableStateFlow(...) 就搞定了。

依赖配置:Gradle 里该加什么

不管选哪个,依赖都得配对。以我最常用的 Mockk 为例,分享一份完整的 Gradle 配置:

// 项目级 build.gradle.kts (root)
allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") } // 有些扩展库需要
    }
}

// 模块级 build.gradle.kts (app)
dependencies {
    // 核心库
    testImplementation("io.mockk:mockk:1.13.10")

    // 如果需要在 Compose 测试中使用(androidTest 或 debugImplementation)
    debugImplementation("io.mockk:mockk-android:1.13.10")

    // 协程支持(默认会随核心库带入,但建议显式声明)
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

    // Compose 测试框架
    debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.0")
    debugImplementation("androidx.compose.ui:ui-test-junit4:1.6.0")

    // 如果你还是想用 Mockito(比如和老项目混用)
    // testImplementation("org.mockito:mockito-core:5.8.0")
    // testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
    // testImplementation("org.mockito:mockito-inline:5.2.0")
}
小技巧: 如果你用的是 Android Instrumented Test(androidTest),记得用 debugImplementation 而不是 testImplementation。我曾经因为写错了 scope,导致测试跑起来报 ClassNotFoundException,排查了半天……后来才发现是 scope 问题。

配置中的几个坑

配置看起来简单,但有几个细节很容易翻车。我一个个说:

  • Mockito + Kotlin 协程: 必须引入 mockito-kotlin 扩展,并且需要手动处理 suspend 函数的返回值。我曾经用一个 thenReturn 去 mock 一个 suspend 函数,结果测试永远超时。后来换成 thenAnswer { ... } 才解决。
  • Mockk 的 @RelaxedMockK 注解: 如果你只是需要一个“哑实现”来占位,这个注解能让所有函数返回默认值(null、空集合等)。但注意——它不会 mock 协程函数,需要配合 @MockK(relaxed = true) 使用。
  • Compose 测试中的 Mock 注入: Compose UI 组件通常通过 ViewModel 或 use case 获取数据。我建议用 ComposeTestRule 配合 AndroidComposeTestRule,然后把 Mock 对象注入到 ViewModelProvider.Factory 里。
注意: 不要在 testImplementation 里引入 mockk-android——它会依赖 Android 环境,而纯单元测试运行在 JVM 上,报错 ClassNotFoundException: android.os.Looper。如果你的 Compose 测试跑在 instrumented test 里,才用 debugImplementation。

框架选择决策流程

我平时接一个 Compose 项目时,会按下面的流程快速决定用哪个框架。这张图能帮你眼熟一下整个配置路线:

Mock 框架选择与配置决策流程 开始选择 纯Kotlin? (无Java遗留) 推荐 Mockk 添加mockk依赖 配置Coroutines Test 注入到Compose TestRule 有Java旧代码? 或选用 Mockito mockito-kotlin扩展 小提示:如果项目混合 Java/Kotlin,两个框架可以共存 Mockito 用于 Legacy Java 类,Mockk 用于新写的 Kotlin 模块
核心思路: 如果项目是纯 Kotlin(包括 Compose),我无脑推荐 Mockk——它跟 Kotlin 的语法贴合度最高,协程支持天然好。如果团队里还有大量 Java 模块,或者你在维持一个老项目,那 Mockito 依然是稳妥之选。两个框架甚至可以共存,只要注意 class 隔离。

实战配置示例:一个完整的 Compose ViewModel 测试

光说不练假把式。我直接给你一段我项目里常用的测试代码,配置好依赖后就能跑:

// 在 androidTest 或 test 目录下
class LoginViewModelTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    // Mockk 方式
    @MockK
    lateinit var loginUseCase: LoginUseCase

    private lateinit var viewModel: LoginViewModel

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxed = true)

        // 通过 Factory 注入 Mock
        viewModel = LoginViewModel(
            loginUseCase = loginUseCase
        )

        // 将 ViewModel 设置到 Compose 测试环境
        composeTestRule.setContent {
            LoginScreen(viewModel = viewModel)
        }
    }

    @Test
    fun testLoginSuccess() = runTest {
        // 使用 coEvery 模拟协程
        coEvery { loginUseCase.execute(any()) } returns Result.success(true)

        // 执行 UI 操作(假设点击登录按钮)
        composeTestRule.onNodeWithTag("login_button").performClick()

        // 验证
        coVerify { loginUseCase.execute(any()) }
    }
}
踩过的坑: 记得在 setUp() 里调用 MockKAnnotations.init(this)——这个我至少忘了两次,然后一直报 lateinit property has not been initialized。另外,如果你的 Compose 组件内部依赖了 Context,需要确保测试环境有正确的 Context 提供,否则会空指针。

最后说两句配置顺序

我把依赖配置的步骤整理成了一个清单,方便你对照检查:

  1. 确定项目定位:纯 Kotlin / 混合项目 / 已有 Mockito 大量代码。
  2. 添加根仓库:将 mavenCentral()、jitpack.io(可选)加入 repositories。
  3. 添加模块依赖:根据测试类型(test / androidTest)添加对应的 mock 框架依赖。
  4. 添加协程测试依赖:kotlinx-coroutines-test 必须,不然 runTest 用不了。
  5. Compose 测试基础设施:ui-test-manifest 和 ui-test-junit4 缺一不可。
  6. 注入 Mock 对象:在 @Before 中初始化 Mock,并通过 ViewModelFactory 传入 Compose 预览。

嗯,到这步,你的 Compose Mock 测试环境就算搭建完毕了。可以放心写测试了。