Mock — 什么是模拟对象以及测试库

作者: IT Sectr 发布日期: 2026-04-10 阅读时间: 8 分钟

Mock — 是一个替代对象,它模拟真实组件的行为,并允许检查与它的交互。与仅仅返回指定值的Stub不同,Mock会记录方法调用的事实、传递的参数和调用次数。根据Mockito (2024)的数据,Mock是Java和Kotlin项目中最流行的Test Double类型,在超过70%的移动应用程序单元测试中使用。

要点

  • Mock — 检查交互的对象:哪些方法被调用,使用哪些参数以及调用次数
  • Mockito — Java和Android项目中创建Mock最流行的库
  • MockK — Kotlin的Mockito替代方案,原生支持协程和密封类
  • Behavior verification — Mock与Stub的关键区别:Mock检查行为而非状态
  • Over-mocking — 主要的反模式:mock应仅用于外部依赖

什么是Mock?

Mock — 是由模拟框架(Mockito、MockK、EasyMock)创建的对象,它模拟接口或类并记录其所有方法调用。开发者设定预期:方法X将以参数Y被调用并返回Z。测试执行后,Mock检查预期是否与实际调用匹配。

这个术语来源于Test Doubles的戏剧隐喻:Mock是一个“模仿者”,它不仅仅站在舞台上(像Dummy那样),而是扮演一个角色并检查与它的交互是否正确。如果被测试的代码没有调用Mock预期的方法,或者使用错误的参数调用了它——测试将失败并显示违反预期的消息。

Mock如何工作

Mock通过框架工厂创建:mockk<MyInterface>()Mockito.mock(MyClass.java)。框架生成一个代理对象,拦截所有方法调用。每次调用都与预先设定的预期(expectations)进行比较。如果调用符合预期——返回指定的值。如果不符合——Mock根据配置返回默认值或抛出异常。

何时需要Mock

当被测试的代码与具有副作用的组件交互时,Mock是必需的:向服务器发送数据、写入数据库、日志记录、分析、导航、显示系统对话框。没有Mock,这些交互就无法在不启动真实基础设施的情况下进行验证。根据Google Testing Blog,Mock是在不启动测试服务器的情况下检查应用程序是否真正发送了分析事件的唯一方法。

Mock和Stub:详细比较

MockStub之间的区别是测试中讨论最多的话题之一。两种类型都替代真实依赖,但方式根本不同。

标准MockStub
主要问题方法是否被调用?返回了什么结果?
验证行为(verify)状态(assert)
数据返回可选必需
示例verify(analytics).logEvent("click")assertEquals(5, repository.getCount())
使用时机副作用数据返回

实用规则:Mock还是不用

选择的简单测试:问自己——“如果我删除这行代码,测试会失败吗?”。如果测试检查返回值——需要Stub(通过assert验证)。如果测试检查代码是否使用正确的参数调用了方法——需要Mock(通过verify验证)。这种二分法源于命令-查询分离模式:改变状态的方法(commands)需要Mock;返回数据的方法(queries)需要Stub。

Mockito和MockK:库的比较

MockitoMockK之间的选择是设置Kotlin Android项目测试栈时的首要决策之一。两个库执行相同的任务,但对Kotlin特定功能的处理方法不同。

Mockito:久经考验的经典

Mockito — 是Java项目的事实标准。5.x版本由于内置的MockMaker,支持final类、静态方法和构造函数的mock对象。对于Kotlin项目,Mockito需要额外配置:用于改进语法的mockito-kotlin扩展、用于final类的mockito-inline。Mockito在没有额外适配器的情况下不支持Kotlin协程和挂起函数。

MockK:Kotlin优先的方法

MockK是专门为Kotlin创建的。它原生支持协程(coEverycoVerify)、密封类、数据类、对象单例和扩展函数。MockK的语法使用带有lambda块的DSL,这在Kotlin代码中看起来很自然。MockK还可以在无需额外配置的情况下模拟属性(property mocking)——这对于使用LiveData、StateFlow和Delegates的Android项目很重要。

kotlin
// 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秒。

Kotlin中的Mock测试示例

我们将考察三种场景:使用Mock依赖测试ViewModel、使用API调用验证测试UseCase以及使用coVerify测试协程。

示例1:带有Mock分析的ViewModel

kotlin
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") }
    }
}

示例2:带有异步验证的UseCase

kotlin
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)
    }
}

示例3:使用ArgumentCaptor验证参数

kotlin
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需要遵守纪律。违反这些规则会使测试变成脆弱的障碍,每次重构都会失效。

仅mock应用程序的外部边界

严格规则:Mock仅为跨越应用程序边界的依赖创建:API客户端、数据库、文件系统、系统服务(LocationManager、BluetoothAdapter、Camera)。应用程序的内部类——领域实体、值对象、简单工具——不应被Mock替换。它们的行为通过真实对象进行测试。

每个测试一个assert/verify

每个测试应包含恰好一个逻辑检查——要么verify(对于Mock),要么assert(对于Stub)。不要在一个测试中混合状态和行为的验证。如果需要同时检查API调用和结果——创建两个具有不同名称的单独测试。这个规则被称为“每个测试一个断言”,源于Kent Beck(2002)的建议。

  • 在MockK中对返回Unit的方法使用relaxUnitFun = true——否则Mock会在未描述的调用上抛出异常
  • 将verify限制在关键调用上——不要验证每个getter和setter,这会使测试变得脆弱
  • 有意识地应用ArgumentMatchers——如果参数对业务逻辑至关重要,any()会隐藏重要细节
  • 不要滥用verifyNoMoreInteractions——这个方法使测试对生产代码中的任何变化都不必要地严格
  • 使用@MockkAnnotations自动初始化Mock对象——这减少了样板代码并提高了可读性

Mock测试的高级技术

除了基本的模拟之外,还有一些高级技术可以解决移动开发中的特定任务:多线程测试、Flow状态检查和真实对象的部分模拟。

使用spyK的部分Mock

Spy(或部分mock)允许创建一个将调用委托给真实实现但允许覆盖特定方法的对象。在MockK中,spyk基于类的真实实例创建:val repo = spyk(InMemoryUserRepository())。通过every设置了预期的调用通过Mock;其余调用通过真实对象。Spy特别适用于测试依赖注入尚未实现的遗留代码,且只需要覆盖一个方法的情况。

使用Turbine测试StateFlow

在基于Jetpack Compose的现代Android项目中,ViewModel通过StateFlow公开状态。MockK允许模拟Flow依赖,而Turbine库简化了发射验证。经典模式:MockK用于返回Flow的UseCase,Turbine用于验证ViewModel的发射。这个技术栈被Android Testing(Google, 2024)文档推荐用于Kotlin协程项目。

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与Mockito有何不同?

Mock — 是一个概念,一种检查行为的Test Double类型。Mockito — 是一个用于在Java和Android中创建Mock对象的库。其他库:MockK(Kotlin)、EasyMock(Java)、Cuckoo(iOS)。

Mock如何与Kotlin协程一起工作?

要使用Mock测试挂起函数,请使用MockK(coEvery / coVerify)或带mockito-kotlin的Mockito。MockK原生支持协程:coEvery定义挂起函数的行为,coVerify验证其在协程内的调用。所有挂起调用必须在runTest(kotlinx-coroutines-test)内执行。

Mock能在重复调用中返回不同的值吗?

可以。在MockK中,使用returnsManyevery { api.getData() } returnsMany listOf(response1, response2)。在Mockito中——链式调用thenReturn(value1).thenReturn(value2)。这对于测试具有不同响应的连续调用行为很有用。

如何在测试之间清除Mock状态?

MockK中,使用带有relaxed = true字段的@MockK注解,并在@After方法中调用clearMocks(mock)。在Mockito中——Mockito.reset(mock)。最佳实践:通过@Before为每个测试创建新的Mock,以消除测试之间的影响。

Mock如何处理Kotlin中的密封类?

MockK能正确处理密封类:every { useCase() } returns Result.Success(data)Mockito不直接支持密封类,需要变通方法。这就是为什么对于Kotlin项目推荐使用MockK而不是Mockito的原因之一。

总结

  • Mock — 检查依赖行为(verify)而非状态(assert)的Test Double类型
  • Mockito — Java/Android的标准,MockK — 支持协程和密封类的Kotlin优先选择
  • 主要规则:Mock用于外部边界(网络、数据库、系统服务),真实对象用于内部类
  • Over-mocking — 主要反模式:过度替换依赖使测试变得脆弱且收益甚微
  • 一个测试——一个逻辑检查:Mock用verify或Stub用assert,但不在一个测试中同时使用
  • ArgumentCaptor / slot — 验证Mock调用参数的正确方法,而不是盲目使用any()
  • 推荐在Kotlin项目中使用MockK:coEverycoVerify无需额外适配器即可原生与协程一起工作

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读