Mock — 是一个替代对象,它模拟真实组件的行为,并允许检查与它的交互。与仅仅返回指定值的Stub不同,Mock会记录方法调用的事实、传递的参数和调用次数。根据Mockito (2024)的数据,Mock是Java和Kotlin项目中最流行的Test Double类型,在超过70%的移动应用程序单元测试中使用。
要点
Mock — 是由模拟框架(Mockito、MockK、EasyMock)创建的对象,它模拟接口或类并记录其所有方法调用。开发者设定预期:方法X将以参数Y被调用并返回Z。测试执行后,Mock检查预期是否与实际调用匹配。
这个术语来源于Test Doubles的戏剧隐喻:Mock是一个“模仿者”,它不仅仅站在舞台上(像Dummy那样),而是扮演一个角色并检查与它的交互是否正确。如果被测试的代码没有调用Mock预期的方法,或者使用错误的参数调用了它——测试将失败并显示违反预期的消息。
Mock通过框架工厂创建:mockk<MyInterface>()或Mockito.mock(MyClass.java)。框架生成一个代理对象,拦截所有方法调用。每次调用都与预先设定的预期(expectations)进行比较。如果调用符合预期——返回指定的值。如果不符合——Mock根据配置返回默认值或抛出异常。
当被测试的代码与具有副作用的组件交互时,Mock是必需的:向服务器发送数据、写入数据库、日志记录、分析、导航、显示系统对话框。没有Mock,这些交互就无法在不启动真实基础设施的情况下进行验证。根据Google Testing Blog,Mock是在不启动测试服务器的情况下检查应用程序是否真正发送了分析事件的唯一方法。
Mock和Stub之间的区别是测试中讨论最多的话题之一。两种类型都替代真实依赖,但方式根本不同。
| 标准 | Mock | Stub |
|---|---|---|
| 主要问题 | 方法是否被调用? | 返回了什么结果? |
| 验证 | 行为(verify) | 状态(assert) |
| 数据返回 | 可选 | 必需 |
| 示例 | verify(analytics).logEvent("click") | assertEquals(5, repository.getCount()) |
| 使用时机 | 副作用 | 数据返回 |
选择的简单测试:问自己——“如果我删除这行代码,测试会失败吗?”。如果测试检查返回值——需要Stub(通过assert验证)。如果测试检查代码是否使用正确的参数调用了方法——需要Mock(通过verify验证)。这种二分法源于命令-查询分离模式:改变状态的方法(commands)需要Mock;返回数据的方法(queries)需要Stub。
在Mockito和MockK之间的选择是设置Kotlin Android项目测试栈时的首要决策之一。两个库执行相同的任务,但对Kotlin特定功能的处理方法不同。
Mockito — 是Java项目的事实标准。5.x版本由于内置的MockMaker,支持final类、静态方法和构造函数的mock对象。对于Kotlin项目,Mockito需要额外配置:用于改进语法的mockito-kotlin扩展、用于final类的mockito-inline。Mockito在没有额外适配器的情况下不支持Kotlin协程和挂起函数。
MockK是专门为Kotlin创建的。它原生支持协程(coEvery、coVerify)、密封类、数据类、对象单例和扩展函数。MockK的语法使用带有lambda块的DSL,这在Kotlin代码中看起来很自然。MockK还可以在无需额外配置的情况下模拟属性(property mocking)——这对于使用LiveData、StateFlow和Delegates的Android项目很重要。
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)
// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user
基准测试(JVM Benchmark, 2024)显示,MockK为Kotlin项目创建mock对象比Mockito快15-20%,因为它直接处理Kotlin字节码而不是Java反射。对于拥有数千个单元测试的项目,构建时间的差异可能很明显:MockK在大型项目的完整测试运行中节省30-60秒。
我们将考察三种场景:使用Mock依赖测试ViewModel、使用API调用验证测试UseCase以及使用coVerify测试协程。
class ProfileViewModelTest {
private val analytics = mockk<AnalyticsService>()
private val repo = mockk<UserRepository>()
private val vm = ProfileViewModel(repo, analytics)
fun `profile opened logs analytics event`() {
every { analytics.logEvent("profile_opened") } returns Unit
vm.onViewCreated()
verify { analytics.logEvent("profile_opened") }
}
}
class SendMessageUseCaseTest {
private val api = mockk<MessagingApi>()
private val useCase = SendMessageUseCase(api)
fun `send message with correct payload`() = runTest {
val message = Message(text = "Hello", userId = 42)
coEvery { api.sendMessage(any()) } returns MessageResult.Sent("msg_1")
val result = useCase.execute(message)
coVerify {
api.sendMessage(match {
it.text == "Hello" && it.userId == 42
})
}
assertTrue(result is MessageResult.Sent)
}
}
class OrderUseCaseTest {
private val api = mockk<OrderApi>()
private val useCase = OrderUseCase(api)
private val slot = slot<OrderRequest>()
fun `order request contains correct items`() = runTest {
coEvery { api.placeOrder(capture(slot)) } returns OrderResult.Placed("order_1")
useCase.execute(listOf("item_a", "item_b"))
assertEquals(2, slot.captured.items.size)
assertEquals("item_a", slot.captured.items[0])
}
}
在移动开发中有效使用Mock需要遵守纪律。违反这些规则会使测试变成脆弱的障碍,每次重构都会失效。
严格规则:Mock仅为跨越应用程序边界的依赖创建:API客户端、数据库、文件系统、系统服务(LocationManager、BluetoothAdapter、Camera)。应用程序的内部类——领域实体、值对象、简单工具——不应被Mock替换。它们的行为通过真实对象进行测试。
每个测试应包含恰好一个逻辑检查——要么verify(对于Mock),要么assert(对于Stub)。不要在一个测试中混合状态和行为的验证。如果需要同时检查API调用和结果——创建两个具有不同名称的单独测试。这个规则被称为“每个测试一个断言”,源于Kent Beck(2002)的建议。
除了基本的模拟之外,还有一些高级技术可以解决移动开发中的特定任务:多线程测试、Flow状态检查和真实对象的部分模拟。
Spy(或部分mock)允许创建一个将调用委托给真实实现但允许覆盖特定方法的对象。在MockK中,spyk基于类的真实实例创建:val repo = spyk(InMemoryUserRepository())。通过every设置了预期的调用通过Mock;其余调用通过真实对象。Spy特别适用于测试依赖注入尚未实现的遗留代码,且只需要覆盖一个方法的情况。
在基于Jetpack Compose的现代Android项目中,ViewModel通过StateFlow公开状态。MockK允许模拟Flow依赖,而Turbine库简化了发射验证。经典模式:MockK用于返回Flow的UseCase,Turbine用于验证ViewModel的发射。这个技术栈被Android Testing(Google, 2024)文档推荐用于Kotlin协程项目。
class SearchViewModelTest {
private val searchUseCase = mockk<SearchUseCase>()
private val vm = SearchViewModel(searchUseCase)
fun `search emits results`() = runTest {
coEvery { searchUseCase.search("android") } returns
flowOf(SearchResult.Success(listOf(Item("Android TDD"))))
vm.search("android")
vm.state.test {
val state = awaitItem()
assertTrue(state.items.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
}
常见问题
Mock — 是一个概念,一种检查行为的Test Double类型。Mockito — 是一个用于在Java和Android中创建Mock对象的库。其他库:MockK(Kotlin)、EasyMock(Java)、Cuckoo(iOS)。
要使用Mock测试挂起函数,请使用MockK(coEvery / coVerify)或带mockito-kotlin的Mockito。MockK原生支持协程:coEvery定义挂起函数的行为,coVerify验证其在协程内的调用。所有挂起调用必须在runTest(kotlinx-coroutines-test)内执行。
可以。在MockK中,使用returnsMany:every { api.getData() } returnsMany listOf(response1, response2)。在Mockito中——链式调用thenReturn(value1).thenReturn(value2)。这对于测试具有不同响应的连续调用行为很有用。
在MockK中,使用带有relaxed = true字段的@MockK注解,并在@After方法中调用clearMocks(mock)。在Mockito中——Mockito.reset(mock)。最佳实践:通过@Before为每个测试创建新的Mock,以消除测试之间的影响。
MockK能正确处理密封类:every { useCase() } returns Result.Success(data)。Mockito不直接支持密封类,需要变通方法。这就是为什么对于Kotlin项目推荐使用MockK而不是Mockito的原因之一。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。