Kotlin之协程(第六趴)——直接测试协程

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

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

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

kotlin 复制代码
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

kotlin 复制代码
@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是非阻塞调用,这意味这它会立即返回,并可以在函数返回后继续运行协程,因此您不能再测试中使用它。例如:

kotlin 复制代码
@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

kotlin 复制代码
@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

kotlin 复制代码
@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秒并使另一个协程超时。

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

csharp 复制代码
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: [...]

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

添加超时

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

TitleRepository.kt

kotlin 复制代码
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)
    }
}

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

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

如果您不想改协程的行为(例如,在集成测试中),则可以改为将runBlocking与所有调度程序的默认实现结合使用。
相关推荐
峥嵘life18 分钟前
DeepSeek本地搭建 和 Android
android
叶羽西19 分钟前
Android14 Camera框架中Jpeg流buffer大小的计算
android·安卓
jiasting20 分钟前
Android 中 如何监控 某个磁盘有哪些进程或线程在持续的读写
android
AnalogElectronic3 小时前
问题记录,在使用android studio 构建项目时遇到的问题
android·ide·android studio
我爱松子鱼3 小时前
mysql之InnoDB Buffer Pool 深度解析与性能优化
android·mysql·性能优化
江上清风山间明月6 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
子非衣10 小时前
MySQL修改JSON格式数据示例
android·mysql·json
有点感觉11 小时前
Android级联选择器,下拉菜单
kotlin
openinstall全渠道统计13 小时前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫13 小时前
一句话说透Android里面的ServiceManager的注册服务
android