【玩转Test】AndroidX Test 介绍,如何测试 ViewModel 与 LiveData

3,465 阅读5分钟

系列文章

前言

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

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

本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics

AndroidX Test

简介

再开始介绍 ViewModelLiveData 的 test 之前,我们先来介绍一下 AndroidX Test

  • 测试库的集合
  • 提供 Android 组件方法,例如 activity ,application
  • 在 local test 和 instrumented test 中均能使用

AndroidX Test 是一个测试库的集合,它提供了测试版本的组件,例如 application 和 activity。

如果想在 local test 使用 application context 怎么办?有了 AndroidX Test ,您可以使用 ApplicationProvider.getApplicationContext() 方法来获取

AndroidX Test 不仅可以提供 application 。使用 AndroidX Test,您可以只写一次 test 代码,然后在 local test 和 instrumentd test 中运行

不使用 AndroidX Test
不使用 AndroidX Test

在不使用 AndroidX Test 之前,local test 和 instrumented test 使用不同的库。例如 application context ,它们是不同的写法

使用 Android Test,您只需要学习一套 API 便可以使用 local test 或 instrumented test

使用

使用 AndroidX Test,您需要引入依赖

dependencies {
    // Other dependencies

    // AndroidX Test - JVM testing
    testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
    testImplementation "org.robolectric:robolectric:$robolectricVersion"
    testImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"


    // AndroidX Test - Instrumented testing
    androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
}

如果需要使用 application 等资源,需要在测试类上使用 @RunWith(AndroidJUnit4::class) 标记

Android JUnit4 Runner 允许 AndroidX Test 在 local test 和 instrumented test 使用不同的依赖

Test ViewModel

我们来创建 ViewModel 的 test,在 ViewModel 类名上唤出 Generate 菜单,选择 test ,选择存储在 local test source 中

接下来我们开始编码,我们创建 getReposByUser_loadReposEvent 方法用于测试获取仓库数据

首先,我们要提供 ViewModel,不同于在 app 编码使用 ViewModelProvider ,test 中可以直接创建 ViewModel 实例

接着我们调用 ViewModel 中待测试的方法

最后我们需要检查结果,这里暂时不写

这里我们没使用 application context ,因此可以不加入 @RunWith(AndroidJUnit4::class) 注解标记

Test LiveData

Test LiveData 两个要素

对于 LiveData,我们主要注意两件事

  • InstantTaskExecutorRule()
  • Observe LiveData

第一步是添加 InstantTaskExecutorRule,它是一个 JUnit rule

JUnit rule 允许在测试运行前后定义代码

如果您要测试 LiveData,则需要使用它

// 使用架构组件同步执行每个任务
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()

使用 InstantTaskExecutorRule 需要引入依赖

testImplementation "androidx.arch.core:core-testing:2.1.0"

observe LiveData 也很重要,我们在 activity 和 fragment 中 observe LiveData 时需要传入 LifecycleOwner。而在 test 中我们是拿不到 LifecycleOwner 的,所以我们需要使用 observeForever 方法,但是要注意需要在合适的位置 remove observer

我们基于上一节的代码进行补充

我们通过断言判断 repos 包裹的 List<Repo> 是否是 null 或者 empty

使用 LiveData 扩展函数精简代码

这样的写法有些繁琐,我们每次测试 LiveData 都有加入这么一大段代码,我们可以通过编写一个扩展函数来简化

扩展函数源码如下

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

使用 @Before 注解

前面我们提到 JUnit rule 允许在测试运行前后定义代码,例如示例中的 repoViewModel 变量,如果多个 test 都需要它,我们可以将其单独抽离出来

lateinit var repoViewModel: RepoViewModel

@Before
fun initRepoViewModel() {
    repoViewModel = RepoViewModel(RepoRepository.getRepository())
}
注意:不要将 ViewModel 声明后立刻赋值,像下面这样
// 错误写法
val repoViewModel = RepoViewModel(RepoRepository.getRepository())

这样会导致所有测试都使用同一个 ViewModel 实例,您应该避免这样。每个测试都应有一个新的测试实例

最终的代码如下

class RepoViewModelTest {

    // 使用架构组件同步执行每个任务
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    lateinit var repoViewModel: RepoViewModel

    @Before
    fun initRepoViewModel() {
        repoViewModel = RepoViewModel(RepoRepository.getRepository())
    }

    @Test
    fun getReposByUser_loadReposEmpty() {
        // When load repos by user
        repoViewModel.getReposByUser("Flywith24")

        // Then 检查 repos 是否为 null 或 empty
        val value = repoViewModel.userRepos.getOrAwaitValue()
        assertThat(value.isNullOrEmpty(), `is`(true))
    }

    @Test
    fun getReposByOrg_loadReposEmpty() {
        // When load repos by organization
        repoViewModel.getReposByOrg("Android")

        // Then 检查 repos 是否为 null 或 empty
        val value = repoViewModel.orgRepos.getOrAwaitValue()
        assertThat(value.isNullOrEmpty(), `is`(true))
    }
}

关于我

我是 Fly_with24