4. Mock框架介绍:Mockito与MockK
做UI测试,绕不开一个核心问题——依赖。
你测试一个登录按钮,它背后要调网络、查数据库、存。真要全部跑起来?那测试就不是测UI了,是在搭整套环境。我早期踩过这个坑,一个登录测试跑3分钟,一半时间在等后端mock服务重启。
所以,我们需要Mock。Mock说白了就是找个替身,你告诉这个替身「当收到这个调用时,直接返回这个结果」。它不干活,只演戏。
Android生态里,Java项目用Mockito,Kotlin项目更多开始用MockK。我们一个一个讲清楚。
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能让你的测试干净、快速、可重复
两种框架选其一,然后把它用透。我见过很多团队换来换去,最后哪套都没用熟。锁定一个,把文档读透,比什么都强。