系列文章
- 【玩转Test】开篇-Android test 介绍
- 【玩转Test】AndroidX Test 介绍,如何测试 ViewModel 与 LiveData
- 【玩转Test】Test Doubles 的概念及如何测试 Repository
前言
❝不会测试的开发不是好开发——鲁迅
❞
一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,本篇是该系列的第三篇,前面我们介绍了如何测试 ViewModel 和 LiveData,今天我们介绍一下如何测试 Repository
测试 Repository 遇到的问题
当您为某个类写单元测试,您只想测试该类的代码。测试 Repository 比较棘手的问题是我们只想测试 Repository 中的代码而不测试其下层的代码
我们简单看一下我们 demo 中 Repository 中的代码
很明显,我们无法单独测试 Repository 而不测试 RepoDataSource
中的代码
您可能有疑惑为什么写单元测试时能够单独测试 Repository 中的代码很重要?这里有一些原因
Repository 的部分代码依赖于其他代码,例如数据库代码可能需要运行在真实的设备上
Repository 依赖的代码如数据库代码或者网络数据代码需要运行一段时间,并且网络请求甚至有失败的可能
Repository 依赖的代码中有 bug 会导致测试失败,但由于我们进行的是 Repository 的单元测试,因此您无法定位其位
测试 Repository 我们希望它能运行很快,我们需要的是 local test
Repository 依赖的数据库或网络请求的代码是 long-running and flaky test
,这意味着您的测试是不可靠的
❝简单来讲,
❞Flaky Tests
是当重复运行相同的代码,有些时候能通过,有些时候不能通过
测试时应该避免这种情况,因为这样的测试结果是不可靠的
那么我们应如何解决该问题呢?答案是 Test Double
Test Doubles 的概念
Test Double
是为测试精心准备的类,它可以在测试中替换真实版本的数据。就像电影中替身演员会替代演员去完成一些危险动作一样。因此在 Repository 中,我们可以为数据源制作 Test Double
事实上,存在很多种类的 Test Double
,本系列文章会介绍 Fake
和 Mock
Fake | 类的有效实现,只适用于测试,不适用于生产 |
---|---|
Mock | 用于跟踪方法调用,根据方法是否被正确的调用来判断测试是否通过 |
Stub | 不包含逻辑并只返回开发者编程返回的逻辑 |
Dummy | 用于传递但并不使用,例如只需要它作为一个参数 |
Spy | 可以跟踪一些其他信息; 例如,如果您创建了SpyTaskRepository,它可能会跟踪 addTask 方法被调用的次数 |
❝如果想了解 Test Double 更详细的信息,请移步 Testing on the Toilet: Know Your Test Doubles
关于 Android 中的 Test Double,可参考 great tips about using test doubles
❞
使用 Fake 意味着数据源不是从网络或者数据库中获取,因此它只适用于测试
测试 Repository
我们可以将 LocalDataSource
和 RemoteDataSource
替换为 FakeDataSource
首先我们在 test source set 中 创建 FakeDataSource
并实现 RepoDataSource
接口
如此一来该接口就有了三个实现类
我们在构造器中传入 Repo list,并完成其内部的获取和保存方法
然后我们便可以编写 RepoRepository 的 test 代码了
在 RepoRepository 上唤出 Generate 弹出框选择 Create Test 选项,这样我们便创建了 RepoRepositoryTest
首先 我们需要提供数据源
class RepoRepositoryTest {
private val repo1 = Repo(id = 1, fork = false)
private val repo2 = Repo(id = 2, fork = false)
private val repo3 = Repo(id = 3, fork = true)
private val repo4 = Repo(id = 4, fork = true)
private val remoteRepos = listOf(repo1, repo2)
private val localRepos = listOf(repo3, repo4)
//...
}
之后我们声明出 RepoRepository
localDataSource 和 remoteDataSource
class RepoRepositoryTest {
//...
private lateinit var localReposDataSource: FakeDataSource
private lateinit var remoteReposDataSource: FakeDataSource
private lateinit var repoRepository: RepoRepository
//...
}
然后我们编写初始化 Repository 的代码
@Before
fun initRepository() {
remoteReposDataSource = FakeDataSource(remoteRepos)
localReposDataSource = FakeDataSource(localRepos)
repoRepository = RepoRepository(remoteReposDataSource, localReposDataSource)
}
最后我们编写 Test 代码,由于 getRepos
是挂起函数,因此我们在这里使用了 runBlocking{}
@Test
fun getRepos() = runBlocking {
val result = repoRepository.getRepos("Flywith24", true)
assertThat(result.value, IsEqual(remoteRepos))
}
关于我
我是 Fly_with24