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。到时候你会发现,这些知识全都能用上。