13、Repository模拟测试:MockK入门、模拟数据层、验证交互
说到Repository层的测试,很多新手会问:我直接调真实数据库不就行了?嗯,想法很美好,现实很骨感。真实数据库有网络延迟、文件读写、状态污染,跑一次测试等半天,还经常因为环境问题报错。我个人的习惯是——能用Mock绝不用真家伙。
这一章我们聊聊MockK这个库。它是Kotlin生态里最流行的Mock框架,语法简洁,对协程支持也好。说白了,就是帮我们造一个假的Repository,让它按剧本演戏,我们只管验证它演得对不对。
为什么需要Mock数据层?
你想想看,Repository层通常依赖网络API、本地数据库、SharedPreferences这些外部资源。测试时如果真去联网,那叫集成测试,不是单元测试。单元测试的核心是快,毫秒级跑完。Mock就是为了这个目的。
我在项目中遇到过好几次:测试因为网络超时而失败,结果发现是后端接口挂了。这种问题不该归单元测试管。Mock掉数据层,测试只关心业务逻辑对不对,至于数据从哪来,那是Mock的事。
MockK快速上手
先加依赖。在build.gradle.kts里加上:
testImplementation("io.mockk:mockk:1.13.8")
然后就可以写测试了。我习惯用mockk<T>()来创建Mock对象,简单直接。
// 假设我们有一个UserRepository接口
interface UserRepository {
fun getUser(id: String): User
fun saveUser(user: User): Boolean
}
// 测试代码
@Test
fun `test mock repository`() {
val mockRepo = mockk<UserRepository>()
// 定义行为:当调用getUser("1")时,返回一个假用户
every { mockRepo.getUser("1") } returns User("1", "张三")
// 调用
val user = mockRepo.getUser("1")
assertEquals("张三", user.name)
}
这里every { ... } returns ...是MockK的核心语法。它告诉Mock对象:当这个方法被这样调用时,你就返回这个值。有点像给演员发剧本。
模拟数据层的几种模式
实际项目中,Repository往往不是接口,而是类。而且可能有多个依赖。我总结了三种常见场景:
场景一:接口模拟
最简单,直接mock接口。适合依赖倒置做得好的项目。
val api = mockk<ApiService>()
val dao = mockk<UserDao>()
val repo = UserRepositoryImpl(api, dao)
场景二:类模拟
如果Repository是具体类,也可以用mockkClass。但要注意,MockK默认不会调用真实构造方法。
val repo = mockkClass(UserRepositoryImpl::class)
every { repo.getUser(any()) } returns User("mock", "Mock用户")
场景三:部分模拟(Spy)
有时候我们想保留真实对象的部分行为,只Mock掉某些方法。这时候用spyk。
val realRepo = UserRepositoryImpl(api, dao)
val spyRepo = spyk(realRepo)
// 只Mock getUser,其他方法走真实逻辑
every { spyRepo.getUser(any()) } returns User("spy", "间谍用户")
验证交互:你调了没?
MockK不仅能模拟返回值,还能验证方法是否被调用、调了几次、传了什么参数。这在测试Repository的交互逻辑时特别有用。
举个例子,用户登录成功后,应该先保存token,再跳转页面。我们想验证Repository的saveToken方法是否被调用了。
@Test
fun `test login saves token`() {
val mockRepo = mockk<UserRepository>(relaxed = true)
val viewModel = LoginViewModel(mockRepo)
viewModel.login("user", "pass")
// 验证 saveToken 被调用了一次,参数是 "abc123"
verify(exactly = 1) { mockRepo.saveToken("abc123") }
}
verify是验证的核心。你可以指定:
exactly = 1:恰好调用一次atLeast = 1:至少一次atMost = 3:最多三次wasNot Called:从未调用
我曾经踩过一个坑:测试里忘了加exactly,结果方法被调了两次我都没发现。后来加了验证,才发现有个循环里重复调用了API。嗯,verify真的是排查重复调用的利器。
协程与MockK
现在的Repository大多用挂起函数。MockK对协程支持很好,用coEvery和coVerify就行。
interface UserRepository {
suspend fun fetchUser(id: String): User
}
@Test
fun `test coroutine repository`() = runTest {
val mockRepo = mockk<UserRepository>()
coEvery { mockRepo.fetchUser("1") } returns User("1", "协程用户")
val user = mockRepo.fetchUser("1")
assertEquals("协程用户", user.name)
coVerify { mockRepo.fetchUser("1") }
}
注意:协程测试要用runTest,这是Kotlin官方提供的测试协程工具。MockK的coEvery会自动处理挂起函数的Mock。
coEvery而用了every,MockK会报错,提示你协程方法需要用co前缀。别问我怎么知道的,我debug了半小时才发现。
Relaxed Mock:懒人福音
有时候我们不想为每个方法都写every。对于不关心的调用,可以用relaxed = true。这样MockK会给所有方法返回默认值(数字返回0,对象返回null,集合返回空列表)。
val mockRepo = mockk<UserRepository>(relaxed = true)
// 不用写 every,直接调用
mockRepo.getUser("1") // 返回 null
mockRepo.saveUser(User("1", "test")) // 返回 false
但要注意,relaxed mock容易掩盖问题。我一般只在验证交互时用,如果关心返回值,还是老老实实写every。
知识体系一览
下面这张图总结了MockK在Repository测试中的核心用法:
实战:完整的Repository测试
最后给一个完整的例子。假设我们有一个OrderRepository,它依赖网络API和本地缓存。我们要测试下单逻辑:先保存到本地,再调用网络,如果网络失败则回滚本地。
class OrderRepositoryTest {
private val api = mockk<OrderApi>()
private val cache = mockk<OrderCache>(relaxed = true)
private val repo = OrderRepository(api, cache)
@Test
fun `test place order success`() = runTest {
val order = Order("123", 99.9)
coEvery { api.placeOrder(order) } returns OrderResult.Success("order_001")
val result = repo.placeOrder(order)
assertTrue(result is OrderResult.Success)
coVerify(exactly = 1) { cache.saveOrder(order) }
coVerify(exactly = 1) { api.placeOrder(order) }
}
@Test
fun `test place order failure rollback`() = runTest {
val order = Order("123", 99.9)
coEvery { api.placeOrder(order) } throws NetworkException("timeout")
val result = repo.placeOrder(order)
assertTrue(result is OrderResult.Failure)
// 验证回滚:缓存被删除
coVerify { cache.deleteOrder(order.id) }
}
}
这个测试里,我们验证了两件事:成功时缓存和API都被调用;失败时缓存被回滚。这就是MockK的价值——你不需要真的连数据库,就能把整个流程测透。
mockk的relaxed模式可以避免这个问题,但记得在关键路径上还是要显式定义行为。
好了,这一章的内容就这些。MockK其实还有很多高级用法,比如参数匹配、答案块、对象Mock等。但掌握了every、verify、coEvery这三个核心,你已经能应对90%的Repository测试场景了。剩下的,遇到具体问题再查文档也不迟。
公众号:蓝海资料掘金营,微信deep3321