使用 Mockk 和 Truth 在 Android 上进行单元

2022-08-12  本文已影响0人  安安_660c

介绍

之前的文章解释了如何开始编写单元测试以及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 框架;您的应用可能仍在使用LiveDataViewModel 来更改 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.

注意:如果你想了解更多关于SharedFlowand的内容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()
    }
}

链接:https://cmhernandezdel.hashnode.dev/tests-unitarios-en-android-con-mockk-y-truth-iii-tests-que-implican-corrutinas-livedata-y-flow

上一篇下一篇

猜你喜欢

热点阅读