【玩转Test】Test Doubles 的概念及如何测试 Repository

3,293 阅读4分钟

系列文章

前言

不会测试的开发不是好开发——鲁迅

一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,本篇是该系列的第三篇,前面我们介绍了如何测试 ViewModel 和 LiveData,今天我们介绍一下如何测试 Repository


本文内容来自 Udacity Advanced Android with Kotlin-Lesson 11-5.2 Testing: Intro to Test Doubles & Dependency Injection

测试 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,本系列文章会介绍 FakeMock


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

我们可以将 LocalDataSourceRemoteDataSource 替换为 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