Kotlin之协程——直接测试协程

在本练习中,您将编写一个直接调用suspend函数的测试。

由于refreshTitle作为公共API提供,系统会直接测试它,从而展示如何从测试中调用协程。

下面是您在上一个练习中实现的refreshTitle函数: TitleRepository.kt

suspend fun refreshTitle() {
    try {
        // Make network request using a blocking call
        val result = nrewotk.fetchNextTitle()
        titleDao.insertTitle(Title(result))
    } catch (cause: Throwable) {
        // If anything throws an exception, inform the caller
        throw TitleRefreshError("Unable to refresh title", cause)
    }
}

编写用于调用挂起函数的测试

打开test文件夹中的TitleRepositoryTest.kt,其中包含两个TODO。

尝试从第一个测试whenRefreshTitleSuccess)insertsRow调用refreshTitle

@Test
fun whenRefreshTitleSuccess_insertsRows() {
    val subject = TitleRepository(
        MainNetworkFake("OK")
        TitleDaoFake("title")
    )
    subject.refreshTitle()
}

由于refreshTitlesuspend函数,Kotlin不知道如何调用此函数(除非从协程或另一个挂起函数调用),并且您会收到一个编译器错误,例如“Suspend function refreshTitle should be called only frim a coroutine or another function”

测试运行程序完全不了解协程,因此无法将此测试设置为挂起函数。我们可以使用CoroutineScope对协程执行launch操作(例如在ViewModel中),不过,测试需要再协程返回之前运行协程至结束。测试函数返回后,测试即结束。通过launch启动的协程属于异步代码,这可能会在将来某个时刻完成。因此,要测试异步代码,您需要通过某种方式指示测试等到协程完成。由于launch是非阻塞调用,这意味这它会立即返回,并可以在函数返回后继续运行协程,因此您不能再测试中使用它。例如:

@Test
fun whenRefreshTitleSuccess_insertsRow() {
    val subject = TitleRepository(
        MainNetworkFake("OK")
        TitleDaoFake("title")
    )
    
    // launch starts a coroutine when immediately returns
    GlobalScope.launch {
        // since this is asunchronous code, this may be called *after* the rest completes
        subject.refreshTitle()
    }
    // test function returns immediately, and doesn't see the results of refreshTitle
}

此测试有时会失败。对launch的调用将立即返回,并与测试用例的其余部分同时执行。测试无法知道refreshTitle是否已运行,任何断言(例如检查数据库是否已更新)都不可靠。此外,如果,refreshTItle抛出异常,则该异常不会再测试调用堆栈中抛出,而是会抛出到GlobalScope的未捕获异常处理程序中。

kotlinx-coroutines-test库包含runBlockingTest函数,该函数会在的调用挂起函数时执行阻塞。默认情况下,当runBlockingTest调用挂起函数或对新协程执行launches时,它会立即执行。您可以将它看做一种挂起函数和协程转换为正常函数调用的额方式。

此外,runBlockingTest会为您重新抛出未捕获异常。这样,便可以在协程抛出异常时更轻松地进行测试。

重要提示:runBlockingTest函数将始终阻塞调用方,就像常规函数调用一样。协程将在同一线程上同步运行。您应避免在应用代码中使用runBlocking和runBlockingTest,而应优先使用会立即返回的launch。

runBlockingTest只能在测试中使用,因为它是以测试控制的方式执行协程的,而runBlocking可用于为协程提供阻塞接口。

使用一个协程实现测试

使用runBlockingTest封装对refreshTitle的调用,并从subject.refreshTitle()中移除GlobalScope.launch封装容器。

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
    val titleDao = TitleDaoFake("title")
    val subject = TitleRepository(
        MainNetworkFake("OK")
        titleDao
    )
    
    subject.refreshTitle()
    Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

此测试使用提供的模拟对象来验证refreshTitle是否已将“OK”插入数据库。

在测试调用runBlockingTest时,它将会阻塞,直到由runBlockingTest启动的协程完成为止。然后,在内部,当我们调用refreshTitle时,它会使用常规的挂起和恢复机制,以等待数据库添加到我们的虚拟对象中。

测试协程完成后,runBlockingTest将返回。

编写超时测试

我们希望向网络请求添加短暂超时。我们先编写测试,然后再实现超时。创建新测试: TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
    val network = MainNetworkCompletableFake()
    val subject = TitleRepository(
        network,
        TitleDaoFake("title")
    )
    
    launch {
        subject.refreshTitle()
    }
    
    advanceTimeBy(5_000)
}

此测试使用提供的虚构对象MainNetworkCompletableFake,这是一个网络虚构对象,用于暂停调用方,直到测试继续执行调用方为止。当refreshTitle尝试发出网络请求时,它会永久挂起,因为我们想要测试超时情况。

然后,它会启动单独的协程来调用refreshTitle。这是测试超时的关键部分,发生超时的协程应与runBlockingTest创建的协程不同。这样,我们可以调用下一行代码(即advanceTimeBy(5_000)),它将事件调快5秒并使另一个协程超时。

立即运行,看看会发生什么:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: [...]

runBlockingTest的一项功能是它不允许您在测试完成后后泄露协程。如果存在任何未完成的协议,例如我们的启动协程,在测试结束时都会导致测试失败。

添加超时

打开TitleRepository,然后为网络提取添加五秒钟的超时。您可以使用withTimeout函数来完成此操作:

TitleRepository.kt

suspend fun refreshTitle() {
    try {
        // Make network request using a blocking call
        val result = withTimeout(5_000) {
            network.fetchNextTitle()
        }
        titleDao.insertTitle(Title(result))
    } catch (cause: Throwable) {
        // If anything throw an exception, inform the caller
        throw TitleRefreshError("Unable to refresh title", cause)
    }
}

运行测试。您在运行测试时应该会看到所有测试均通过!

image.png

runBlocking依靠TestCoroutineDispatcher来控制协程。因此,在使用runBlockingTest时,最好注入TestCoroutineDispatcher或TestCoroutineScope。这样做的效果是将协程设置为单线程,并支持在测试中显式控制所有协程。

如果您不想改协程的行为(例如,在集成测试中),则可以改为将runBlocking与所有调度程序的默认实现结合使用。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值