介绍
之前的文章解释了如何开始编写单元测试以及Mockk的一些更高级的特性。一旦你了解了这一点,就有必要继续测试稍微复杂的东西,尤其是当我们要测试ViewModel类型的类时。本文收集了实现它所需的所有“技巧”。
Testeando corrutinas
当我们使用 ViewModel 时,对于许多功能来说,它必须是这样的,这是很正常的:
fun doSomething() = viewModelScope.launch {
...
}
注意:如果你想了解更多关于协程、作用域等的知识,可以从这里开始。
首先,尝试suspend
在正常测试中调用函数会产生编译时错误,因为测试也应该是一个suspend function
。为此,我们有一个新关键字runTest
. 让我们看看它是如何使用的:
@Test
fun testSomething() = runTest {
// your test that uses coroutines here
}
有了这个,我们正在创建一个执行测试的范围,所以我们已经可以suspend functions
在其中使用它了。但是,如果我们尝试测试使用viewModelScope.launch { ... }
它的函数,我们可能会发现一个错误,告诉我们有关调度程序的一些信息。为此,我们必须更改Dispatcher
,这样做是这样的:
@Before
fun setUp() {
Dispatchers.setMain(UnconfinedTestDispatcher())
// all your other initialization code here
}
@After
fun tearDown() {
Dispatchers.resetMain()
// all your other cleanup code here
}
@Test
fun testSomething() = runTest {
// call your test here
}
有了这个,我们可以为我们的 ViewModel 中使用viewModelScope.launch { ... }
.
注意:如果您想了解更多关于 Dispatchers、替代品UnconfinedTestDispatcher
等的信息,可以访问这篇文章。
测试 LiveData 值
虽然看起来想法是逐渐用Flows代替它们,这是 Kotlin 的典型并且不使用 Android 框架;您的应用可能仍在使用LiveData
ViewModel 来更改 UI 中的内容。如果是这样,您在请求 LiveData 值以检查其结果时可能会遇到竞争条件。
幸运的是,没有什么是无法修复的,为此我们将编写以下扩展函数:
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@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
}
基本上这个函数的作用是我们可以向 LiveData 请求一个值,最多等待 2 秒让它改变。如果值没有改变,则该方法抛出异常,如果改变,则返回该值。
我们可以这样调用该方法:
@Test
fun testSomething() = runTest {
// call your method that changes some live data value
val result = yourLiveData.getOrAwaitValue()
assertThat(result).isEqualTo(5)
}
但是,如果我们按原样这样做,我们会发现代码中引发了异常。这是因为我们需要添加一个规则,它只是测试类开头的几行代码。
让我们看一个例子。如果我们有一个显示汽车细节的视图,UI 中的灯在引擎运行时打开,绑定到 aLiveData<Boolean>
并且我们想要测试它的行为,我们将执行以下操作:
class CarDetailViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var viewModel: CarDetailViewModel
@Before
fun setUp() {
viewModel = CarDetailViewModel()
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `Turn on engine sets the isOn LiveData to true`() = runTest {
viewModel.turnOnEngine()
val liveDataValue = viewModel.isOn.getOrAwaitValue()
assertThat(liveDataValue).isTrue()
}
}
我们感兴趣的行是这些:
@get:Rule
val rule = InstantTaskExecutorRule()
有了这个,我们已经可以对 LiveData 类型的变量进行测试了。
Nota para MediatorLiveData
如果您使用MediatorLiveData
组合不同的值MutableLiveData
,您将看到您的测试失败。这是因为MediatorLiveData
如果没有人在看,a 永远不会更新,所以我们需要首先observer
在测试中创建 a:
@Test
fun testSomething() = runTest {
// Arrange
viewModel.myMediatorLiveData.observeForever {}
// Update the mediator live data sources
// Act
// Assert
}
完成此操作后,我们可以管理MediatorLiveData
.
测试流程
为了完成这篇文章,我们将看到另一个可以执行测试的典型组件:流。例如,一个典型的用例是使用SharedFlow
向 UI 发送事件,或使用StateFlow
而不是LiveData
.
注意:如果你想了解更多关于SharedFlow
and的内容StateFlow
,可以看下面的文章。
为了测试 Flows,我们需要一个名为Turbine的新库。我们可以像添加任何其他库一样在build.gradle
应用程序模块中添加它:
testImplementation 'app.cash.turbine:turbine:0.9.0'
这个库所做的是向 Flows 添加一个扩展函数,称为test
,以及更多期望值的函数等。
让我们通过一个示例来看看如何检查在 a 中发出的值SharedFlow
(使用我们在 LiveData 部分中看到的相同示例):
@Test
fun `Turn on engine emits true in SharedFlow`() = runTest {
viewModel.isOn.test {
viewModel.turnOnEngine()
val item = awaitItem()
assertThat(item).isTrue()
}
}
需要注意的重要一点是,如果一个项目从未发出过,则测试不会失败,而是会挂起。为此,我们可以将参数传递给runTest
,称为dispatchTimeoutMs
,如果测试未在该时间内完成,这将导致测试失败。唯一的变化是这样的:
@Test
fun `Turn on engine emits true in SharedFlow`() = runTest(dispatchTimeoutMs = 50) {
viewModel.isOn.test {
viewModel.turnOnEngine()
val item = awaitItem()
assertThat(item).isTrue()
}
}