使用 Mockk 和 Truth 在 Android 上进行单元测试(III):涉及协程、LiveData 和 Flow 的测试

介绍

之前的文章解释了如何开始编写单元测试以及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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容