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对象 Stubbing方法调用 验证交互行为 @Mock 注解 / mock() 静态方法 @InjectMocks 自动注入 thenReturn / thenThrow / thenAnswer ArgumentMatchers (any, eq, argThat) times / never / atLeastOnce timeout / verifyNoMoreInteractions Kotlin扩展:mockito-kotlin / mockito-inline 解决Kotlin默认final、反引号、类型推断等问题

好了,这一章的内容就到这里。Mockito的核心就是三件事:创建mock、打桩、验证。把这三件事搞明白,你就能写出干净、可靠的单元测试了。下一章我们会深入Kotlin协程的测试,那又是另一番天地了。

公众号:蓝海资料掘金营,微信deep3321