系列文章
【玩转 Test】开篇-Android test 介绍 【玩转 Test】AndroidX Test 介绍,如何测试 ViewModel 与 LiveData 【玩转 Test】Test Doubles 的概念及如何测试 Repository
前言
❝不会测试的开发不是好开发——鲁迅
❞
一直以来,关于如何写测试代码的相关内容资源都比较少,之前在优达学城看到了这部分的视频,但由于没有中文字幕,对有些小伙伴可能不太友好。因此我决定将其整理成系列文章,本篇是该系列的第二篇,我们来介绍一下 AndroidX Test
以及如何对 ViewModel 和 LiveData 进行测试
本文内容来自 Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics
AndroidX Test
简介
再开始介绍 ViewModel
和 LiveData
的 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
之前,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