4. Mock框架介绍:Mockito与MockK

做UI测试,绕不开一个核心问题——依赖。

你测试一个登录按钮,它背后要调网络、查数据库、存。真要全部跑起来?那测试就不是测UI了,是在搭整套环境。我早期踩过这个坑,一个登录测试跑3分钟,一半时间在等后端mock服务重启。

所以,我们需要Mock。Mock说白了就是找个替身,你告诉这个替身「当收到这个调用时,直接返回这个结果」。它不干活,只演戏。

Android生态里,Java项目用Mockito,Kotlin项目更多开始用MockK。我们一个一个讲清楚。

Mock框架选择决策树 你的项目语言是什么? Java Kotlin Mockito MockK 核心特点 • 老牌成熟,社区庞大 • when/thenReturn 语法 • 需要 mockito-inline 支持 final 核心特点 • Kotlin 原生,支持空安全 • coEvery/coVerify 协程支持 • relaxed mock 模式 推荐:Kotlin 项目优先 MockK Java 项目或混合项目可继续用 Mockito Android UI测试

4.1 为什么需要Mock框架?

单元测试的基本原则是——只测当前单元,不测依赖。

比如测一个ViewModel的登录方法,它内部调用了网络层接口。你的关注点是:用户点击登录后,ViewModel是否正确处理了Loading状态?而不是「网络请求到底有没有发出去」。

Mock框架帮你切断外部依赖,让你能精准验证当前模块的行为。

核心价值

  • 隔离外部依赖:网络、数据库、系统服务
  • 控制输入输出:指定某个方法返回特定值
  • 验证交互行为:这个方法到底被调用了没有?参数对不对?
  • 加速执行:没有IO,毫秒级跑完

4.2 Mockito —— 老牌劲旅

Mockito在Java世界几乎统治了十年。我不夸张地说,我2015年入行时,所有测试代码里90%都在用Mockito。

它的核心思路很简单:创建Mock对象,设定行为,验证调用。

4.2.1 基础用法

// 1. 创建Mock对象
lateinit var mockRepository: UserRepository
@Before
fun setup() {
    mockRepository = mock(UserRepository::class.java)
}

// 2. 设定行为
@Test
fun `当用户名合法时,登录成功`() {
    `when`(mockRepository.login("test", "123456"))
        .thenReturn(Result.success(User("test")))

    val result = viewModel.login("test", "123456")

    assertTrue(result.isSuccess)
}

// 3. 验证交互
@Test
fun `登录失败时,展示错误提示`() {
    `when`(mockRepository.login(any(), any()))
        .thenReturn(Result.failure(NetworkException()))

    viewModel.login("error", "xxx")

    verify(mockRepository).login("error", "xxx")
    assertTrue(viewModel.errorMessage.value.isNotEmpty())
}

我的个人习惯:Mockito的 `any()` 很强大,但小心滥用。我刚开始总喜欢 `any()` 一把梭,后来发现测试根本测不到边界。能用具体值就用具体值,只在无关参数上使用 `any()`。

4.2.2 Mockito在Material 3测试中的常见场景

你测试一个BottomSheet组件,这个组件要从ViewModel里读数据。Mockito可以帮你这样搞:

@Test
fun `BottomSheet展示用户列表时,应显示正确的Item数量`() {
    val mockViewModel = mock(UserListViewModel::class.java)
    `when`(mockViewModel.users).thenReturn(
        MutableStateFlow(listOf(user1, user2, user3))
    )

    composeTestRule.setContent {
        MaterialTheme {
            UserListBottomSheet(mockViewModel)
        }
    }

    composeTestRule.onAllNodesWithTag("user_item")
        .assertCountEquals(3)
}

4.2.3 避坑指南

我曾经踩过一个坑:Mockito默认不能mock final class。Kotlin的类默认是final的。如果你不引入 `mockito-inline` 或者 `mockito-kotlin`,直接mock会报错。这个问题在Mockito 2.x时代非常普遍,升级到4.x后情况好多了,但最好还是显式加上依赖:

testImplementation "org.mockito:mockito-inline:5.2.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:5.1.0"

4.3 MockK —— Kotlin原生Mock框架

Kotlin项目用Mockito总有种「隔靴搔痒」的感觉。比如协程的支持不够自然,空安全处理起来也略显笨拙。MockK就是为解决这个而生的。

我个人在2020年切换Kotlin项目后,第一时间就把Mockito换成了MockK。舒服太多了。

4.3.1 基础用法

// 1. 创建Mock(语法更简洁)
val mockRepository = mockk<UserRepository>()

// 2. 设定行为(every 替代 when)
@Test
fun `登录成功返回用户信息`() {
    every { mockRepository.login("test", "123") }
        .returns(Result.success(User("test")))

    val result = viewModel.login("test", "123")

    assertEquals("test", result.getOrNull()?.name)
}

// 3. 验证(verify 替代 verify)
@Test
fun `登录失败时验证调用次数`() {
    every { mockRepository.login(any(), any()) }
        .returns(Result.failure(NetworkException()))

    viewModel.login("a", "b")

    verify(exactly = 1) { mockRepository.login("a", "b") }
}

4.3.2 协程支持 —— 这很重要

Material 3的很多状态更新都是协程驱动的。比如一个AnimatedVisibility的动画状态管理,背后可能是一个Flow在驱动。

MockK对协程有原生支持,你不需要额外写任何适配代码:

@Test
fun `协程场景测试`() = runTest {
    val mockApi = mockk<ApiService>()
    // 注意这里用的是 coEvery
    coEvery { mockApi.fetchUsers() }
        .coReturns(listOf(user1, user2))

    val viewModel = UserViewModel(mockApi)
    viewModel.loadUsers()

    // coVerify 用于验证协程方法
    coVerify { mockApi.fetchUsers() }
    assertEquals(2, viewModel.users.value.size)
}

我建议:如果你的ViewModel里用 `viewModelScope.launch`,测试时务必使用 `runTest` 配合 `TestCoroutineDispatcher`。MockK的 `coEvery` 和 `coVerify` 能无缝衔接。

4.3.3 relaxed mock 模式

有一个我特别喜欢的MockK特性——relaxed mock。默认情况下,如果你没有设定某个方法的行为,MockK会抛异常。但relaxed模式会「宽容处理」,返回默认值(0、空集合、null等)。

// relaxed模式:未设定的方法返回默认值
val mock = mockk<UserRepository>(relaxed = true)

// 这行不会报错,虽然我们没有设定 updateUser 的行为
mock.updateUser(User("test"))
// verify 依然可以验证它是否被调用过
verify { mock.updateUser(any()) }

这在测试比较复杂的UI组件时特别有用——你只关心部分依赖,让其他依赖保持「静默无异常」状态。

注意:relaxed mock用多了会降低测试的严谨度。我一般只在大组件集成测试里用,单元测试还是坚持explicit mock,每个行为都显式定义。

4.4 Mockito vs MockK:如何选择?

维度 Mockito MockK
语言支持 Java 主,Kotlin 需适配 Kotlin 原生,空安全
协程支持 需 mockito-kotlin 扩展 原生 coEvery / coVerify
语法简洁度 when/thenReturn every/returns
final类支持 需要 mockito-inline 原生支持
学习曲线 平缓,资料丰富 稍高,但Kotlin开发者上手快
社区生态 极其庞大 活跃,Kotlin社区首选
Material 3 测试适配 可行,需额外配置 更顺滑,协程场景尤其

我个人建议:

  • 纯 Java 项目或遗留混合项目 → 用 Mockito,别折腾
  • 新开 Kotlin 项目、重度使用协程/Flow → 优先选 MockK
  • 两者可以共存,但别在同一个模块里混用,维护成本会翻倍

4.5 在Compose UI测试中使用Mock

讲回到Material 3组件测试。Compose测试的环境天然需要mock。

比如你测试一个BottomSheet里的列表,数据从ViewModel来。这时候你可以:

class BottomSheetTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    private lateinit var mockViewModel: ItemListViewModel

    @Before
    fun setup() {
        // 使用MockK
        mockViewModel = mockk(relaxed = true)
        every { mockViewModel.items } returns
            MutableStateFlow(listOf(item1, item2))
    }

    @Test
    fun `当BottomSheet展开时,显示加载状态`() {
        composeTestRule.setContent {
            MaterialTheme {
                ItemBottomSheet(mockViewModel)
            }
        }

        composeTestRule.onNodeWithTag("loading_indicator")
            .assertIsDisplayed()
    }

    @Test
    fun `当数据加载完成,显示列表`() {
        composeTestRule.setContent {
            MaterialTheme {
                ItemBottomSheet(mockViewModel)
            }
        }

        composeTestRule.onNodeWithText(item1.title)
            .assertIsDisplayed()
    }
}

这里的关键点是:你不关心ViewModel内部是怎么工作的。你只关心「当items是某个值时,UI展现出什么」。这就是Mock的威力——把ViewModel变成一个可预测的数据源。

4.6 本章总结

一句话记住:

  • Mockito 是Java的经典选择,稳定可靠
  • MockK 是Kotlin的最佳搭子,协程满分
  • Material 3测试中,用好Mock能让你的测试干净、快速、可重复

两种框架选其一,然后把它用透。我见过很多团队换来换去,最后哪套都没用熟。锁定一个,把文档读透,比什么都强。