4、Mockito框架入门:Mock与Spy的区别、when-thenReturn、verify、ArgumentMatcher、Answer接口

好,咱们进入Mockito的世界。说实话,这是Android单元测试里最实用的工具之一。我见过太多人把Mockito用成了“魔法”,出了问题一脸懵。今天咱们就把它的核心概念掰开揉碎,讲清楚。

4.1 Mock与Spy:一个“假人”,一个“半假人”

先问个问题:你写单元测试时,最烦什么?我猜是那些依赖——网络请求、数据库、SharedPreferences。Mock和Spy就是用来对付它们的。

Mock,说白了就是创建一个“假对象”。它所有的方法默认什么都不做,返回默认值(int返回0,对象返回null)。你让它怎么演,它就怎么演。

Spy呢?它是个“半假人”。它基于真实对象创建,默认会调用真实方法。但你可以选择性地“打桩”,让某些方法返回假数据。

核心区别一句话:Mock是彻底伪造,Spy是真实对象的代理。

我在项目中遇到过这样一个坑:用Spy去监控一个复杂的业务对象,结果忘了某个方法会被真实调用,导致测试跑起来居然真的去访问了数据库。嗯,从那以后我养成了一个习惯——能用Mock的地方绝不用Spy。

什么时候用Spy?我个人习惯是:当你需要保留大部分真实行为,只修改个别方法时。比如测试一个缓存类,你只想让“从网络获取数据”的方法返回假数据,其他逻辑保持真实。

// 创建Mock
val mockList = mock(List::class.java)
mockList.size // 返回0,因为没打桩

// 创建Spy
val realList = mutableListOf("A", "B", "C")
val spyList = spy(realList)
spyList.size // 返回3,因为调用了真实方法

// 对Spy打桩
`when`(spyList.get(0)).thenReturn("X")
spyList.get(0) // 返回"X",走的是打桩逻辑
spyList.get(1) // 返回"B",走的是真实逻辑

注意:Spy如果用不好,很容易让测试变成“集成测试”。我曾经见过一个团队,整个测试套件里全是Spy,结果跑一次测试要十几秒——因为很多真实逻辑都被执行了。记住,单元测试的核心是“隔离”。

4.2 when-thenReturn:给Mock“编剧本”

你想想看,Mock创建出来就是个空壳子。你得告诉它:当某个方法被调用时,应该返回什么。这就是when-thenReturn的活儿。

// 基本用法
val mockUserRepo = mock(UserRepository::class.java)
`when`(mockUserRepo.getUserName(1)).thenReturn("张三")

// 链式调用:第一次返回"张三",第二次返回"李四"
`when`(mockUserRepo.getUserName(1))
    .thenReturn("张三")
    .thenReturn("李四")

// 或者用thenReturn的变体
`when`(mockUserRepo.getUserName(1))
    .thenReturn("张三", "李四")

这里有个细节:when里面的调用,其实是在“记录”一个方法调用。Mockito会记住这个调用,然后当你调用thenReturn时,它就把这个返回值绑定上去。说白了,就是“录制-回放”模式。

我的习惯:每个测试方法里,when-thenReturn不要超过3个。如果超过3个,说明你的测试可能太复杂了,或者被测对象承担了太多职责。这时候我会考虑重构代码。

4.3 verify:验证“剧本有没有按预期演”

Mockito的verify是我最常用的功能之一。它用来验证:某个方法是否被调用了?调用了多少次?参数是什么?

你可能会问:我为什么要验证这些?嗯,因为有时候代码逻辑正确,但交互顺序错了。比如你写了一个登录流程,先验证token再请求用户信息。如果顺序反了,虽然结果可能一样,但逻辑是有问题的。

// 验证方法被调用了一次
verify(mockUserRepo).getUserName(1)

// 验证方法被调用了2次
verify(mockUserRepo, times(2)).getUserName(1)

// 验证方法从未被调用
verify(mockUserRepo, never()).getUserName(2)

// 验证方法至少被调用1次
verify(mockUserRepo, atLeastOnce()).getUserName(1)

// 验证方法最多被调用3次
verify(mockUserRepo, atMost(3)).getUserName(1)

重要原则:不要过度使用verify。我见过有人每个测试里写七八个verify,把测试变成了“调用记录检查器”。记住,verify是用来验证“关键交互”的,不是用来检查所有调用的。

我曾经犯过一个错误:在测试里verify了一个内部工具类的调用,结果重构时把这个工具类换掉了,所有测试都红了。从那以后,我只verify那些“对外暴露的接口”或者“与外部系统交互”的方法。

4.4 ArgumentMatcher:灵活匹配参数

有时候你并不关心方法被调用时的具体参数值。比如你只关心“调用了save方法”,至于保存的是哪个用户,无所谓。这时候ArgumentMatcher就派上用场了。

// 匹配任意int
verify(mockUserRepo).getUserName(anyInt())

// 匹配任意String
verify(mockUserRepo).saveUser(anyString())

// 匹配任意对象
verify(mockUserRepo).processUser(any(User::class.java))

// 更精确的匹配:参数大于0
verify(mockUserRepo).getUserName(argThat { it > 0 })

// 匹配null
verify(mockUserRepo).saveUser(isNull())

注意:如果你在when里用了参数匹配器,那么所有参数都必须用匹配器。不能混着用。比如:when(mock.method(anyInt(), "固定值")) 这样会报错。正确写法是:when(mock.method(anyInt(), eq("固定值")))

我个人习惯是:在verify里多用参数匹配器,在when里尽量用具体值。为什么?因为when是定义行为,具体值能让测试更清晰;而verify是验证交互,有时候你确实不关心具体参数。

4.5 Answer接口:自定义返回值逻辑

有时候thenReturn不够用。比如你想根据输入参数动态计算返回值,或者你想在方法调用时执行一些副作用(比如修改某个变量)。这时候就需要Answer接口了。

// 根据输入参数动态返回
`when`(mockCalculator.add(anyInt(), anyInt())).thenAnswer { invocation ->
    val a = invocation.getArgument<Int>(0)
    val b = invocation.getArgument<Int>(1)
    a + b + 100  // 在真实结果上加100
}

// 执行副作用
val mutableList = mutableListOf<String>()
`when`(mockList.add(anyString())).thenAnswer { invocation ->
    val item = invocation.getArgument<String>(0)
    mutableList.add(item)
    true
}

我在项目中遇到过这样一个场景:测试一个缓存模块,需要模拟“第一次调用返回null,第二次调用返回缓存数据”。用thenReturn链式调用可以实现,但用Answer更灵活:

var callCount = 0
`when`(mockCache.get(anyString())).thenAnswer {
    callCount++
    if (callCount == 1) null else "cached_data"
}

建议:能用thenReturn解决的问题,就别用Answer。因为Answer让测试逻辑变得更复杂,可读性下降。我只有在需要“根据参数动态计算”或者“执行副作用”时才用它。

4.6 实战小技巧:组合使用

好了,咱们把上面这些知识点串起来,看一个完整的测试例子:

class UserViewModelTest {

    @Test
    fun `加载用户信息时,先验证token再请求用户数据`() {
        // 1. 创建Mock
        val mockAuthService = mock(AuthService::class.java)
        val mockUserRepo = mock(UserRepository::class.java)
        val viewModel = UserViewModel(mockAuthService, mockUserRepo)

        // 2. 打桩
        `when`(mockAuthService.isTokenValid()).thenReturn(true)
        `when`(mockUserRepo.getUser(anyInt())).thenReturn(User(1, "张三"))

        // 3. 执行被测方法
        viewModel.loadUser(1)

        // 4. 验证交互顺序
        val inOrder = inOrder(mockAuthService, mockUserRepo)
        inOrder.verify(mockAuthService).isTokenValid()
        inOrder.verify(mockUserRepo).getUser(1)

        // 5. 验证结果
        assertEquals("张三", viewModel.userName.value)
    }
}

这个例子展示了:Mock创建、when-thenReturn打桩、verify验证交互、ArgumentMatcher匹配参数。你想想看,如果没有Mockito,你要怎么测试这个ViewModel?你得真的去启动一个AuthService,真的去访问数据库。那还叫单元测试吗?

总结一下:Mockito的核心就四个字——“隔离依赖”。Mock和Spy是工具,when-thenReturn是剧本,verify是检查,ArgumentMatcher和Answer是进阶玩法。掌握这些,你已经能写80%的单元测试了。

下一章咱们会讲“如何用Mockito测试Android特有的组件”,比如Activity、Fragment、ViewModel。到时候你会发现,这些知识全都能用上。