3、Mockito框架与Kotlin实践
好,咱们今天来聊聊Mockito。说实话,这个框架是我在Android开发中用得最顺手的测试工具之一。你想想看,写单元测试最头疼的是什么?就是那些依赖——网络请求、数据库、SharedPreferences……要是每次跑测试都得真去调接口,那测试就变成集成测试了,慢得要命。
Mockito就是来解决这个问题的。它帮你创建"假对象",让这些假对象按照你的意愿返回结果。说白了,就是"模拟"。
核心思想: 用假对象替代真实依赖,只测试当前类的逻辑,不关心外部组件的行为。
3.1 Mockito核心原理
Mockito的原理其实不复杂。它利用Java的动态代理机制(对接口)或者CGLIB/ByteBuddy(对类),在运行时生成一个代理对象。这个代理对象的所有方法调用,都会被拦截下来,然后根据你预设的"规则"来返回结果。
我刚开始用Mockito时,总觉得它像"黑魔法"。后来看了源码才明白,说白了就是字节码增强。嗯,这里有个关键点:Mockito不能mock final类和方法(除非你用Mockito 2.x以上的inline版本)。我在项目中就踩过这个坑——有个工具类用了final修饰,死活mock不了,最后只能改成接口。
小提示: Kotlin中所有类和方法默认都是final的。如果你用Kotlin写代码,记得给要mock的类加上open关键字,或者用mockito-inline扩展库。
3.2 创建Mock对象
创建mock对象有两种方式。我个人习惯用注解,代码更干净。
// 方式一:静态方法
val userRepo = mock(UserRepository::class.java)
// 方式二:注解(推荐)
@Mock
lateinit var userRepo: UserRepository
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
}
这里有个细节要注意:MockitoAnnotations.openMocks(this)必须在@Before里调用。我曾经忘了写这行,结果跑了半天测试全是空指针,气得我差点砸键盘。
3.3 Stubbing方法调用
Stubbing就是"打桩"——告诉mock对象,当某个方法被调用时,返回什么结果。
// 基本用法
`when`(userRepo.getUserName(123)).thenReturn("张三")
// 多次调用返回不同结果
`when`(userRepo.getUserName(456))
.thenReturn("李四")
.thenReturn("王五")
// 抛出异常
`when`(userRepo.getUserName(999)).thenThrow(RuntimeException("用户不存在"))
你可能会问:为什么when要用反引号包起来?因为Kotlin里when是关键字。这是Kotlin和Mockito配合时的一个小坑,我第一次写的时候编译器直接报错,愣了几秒才反应过来。
注意: 如果你没有对某个方法调用做stubbing,mock对象会返回默认值——int返回0,boolean返回false,对象返回null。这很容易导致空指针,所以一定要对所有可能被调用的方法做stubbing。
3.4 验证交互行为
验证是Mockito的另一大核心功能。它用来检查:某个方法是否被调用了?调了几次?传了什么参数?
// 验证方法被调用
verify(userRepo).getUserName(123)
// 验证调用次数
verify(userRepo, times(2)).getUserName(123)
verify(userRepo, never()).getUserName(999)
verify(userRepo, atLeastOnce()).getUserName(123)
// 验证超时
verify(userRepo, timeout(100)).getUserName(123)
我个人经验是:验证不要滥用。只验证那些你真正关心的交互,比如"是否保存了数据"、"是否调用了网络请求"。如果每个方法都验证一遍,测试代码会变得又臭又长,维护起来很痛苦。
3.5 ArgumentMatchers使用
有时候你并不关心具体传了什么参数,或者参数是动态生成的。这时候就用ArgumentMatchers。
// 匹配任意参数
`when`(userRepo.getUserName(anyInt())).thenReturn("默认用户")
// 匹配特定类型的参数
`when`(userRepo.saveUser(any(User::class.java))).thenReturn(true)
// 匹配字符串开头
`when`(userRepo.findUsers(startsWith("张"))).thenReturn(listOf(...))
// 综合使用
verify(userRepo).saveUser(argThat { user -> user.age > 18 })
嗯,这里有个大坑:如果你在stubbing或验证中使用了ArgumentMatchers,那么所有参数都必须用matcher。不能混着来——比如一个参数用anyInt(),另一个参数直接写死值。Mockito会直接抛异常。我当初调试这个问题花了整整一下午,最后发现是忘了把另一个参数也改成matcher。
3.6 Kotlin中的Mockito扩展
Kotlin和Mockito配合,有几个扩展库能让生活更美好。
| 扩展库 | 解决的问题 | 使用场景 |
|---|---|---|
| mockito-kotlin | 提供Kotlin友好的API,避免反引号 | 通用场景 |
| mockito-inline | 支持mock final类和方法 | Kotlin默认final类 |
| mockito-android | 解决Android SDK类的mock问题 | Android单元测试 |
用mockito-kotlin后,代码会清爽很多:
// 不用反引号了
whenever(userRepo.getUserName(any())).thenReturn("张三")
// 更简洁的mock创建
val userRepo = mock<UserRepository>()
// 参数匹配也更自然
verify(userRepo).getUserName(eq(123))
我个人强烈推荐在Kotlin项目中使用mockito-kotlin。它不是什么大改动,但能让代码读起来舒服很多。你想想看,每次写`when`都要按Shift+波浪号,多烦人。
3.7 @Mock与@InjectMocks注解
这两个注解是Mockito的黄金搭档。@Mock创建mock对象,@InjectMocks把mock对象注入到被测类中。
class UserViewModelTest {
@Mock
lateinit var userRepo: UserRepository
@Mock
lateinit var logger: Logger
@InjectMocks
lateinit var viewModel: UserViewModel
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
}
@Test
fun `test getUserName`() {
`when`(userRepo.getUserName(1)).thenReturn("张三")
val result = viewModel.getUserName(1)
assertEquals("张三", result)
verify(userRepo).getUserName(1)
}
}
@InjectMocks的工作原理是:它会尝试把@Mock标记的对象,通过构造函数、setter方法或字段注入到被测对象中。如果注入失败,它会静默跳过——这其实是个隐患。我曾经遇到过一个bug,因为构造函数参数类型匹配错了,@InjectMocks注入了一个null进去,测试跑起来全是空指针,排查了好久才发现。
我的建议: 尽量用构造函数注入,少用字段注入。构造函数注入更明确,也更容易排查问题。如果被测类有多个构造函数,记得用@InjectMocks的@InjectMocks(constructorParams)指定用哪个。
知识体系总览
下面这张图总结了Mockito在Kotlin测试中的核心脉络,你可以对照着回顾一下:
好了,这一章的内容就到这里。Mockito的核心就是三件事:创建mock、打桩、验证。把这三件事搞明白,你就能写出干净、可靠的单元测试了。下一章我们会深入Kotlin协程的测试,那又是另一番天地了。