You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单元测试中如何断言Kotlin协程未完成?

问题描述

我需要测试一个挂起函数,断言它在特定条件下不会返回结果(即保持挂起状态)。我尝试了三种扩展函数方案,但都存在问题:


第一种方案

// 扩展函数
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
    val result = async { block }
    advanceTimeBy(5000L)
    val isBlockComplete = result.isCompleted
    assertThat(isBlockComplete, equalTo(false))
}

// 使用示例
@Test
fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
    runTest {
        assertNotCompleted {
            someClass.waitForValue() 
        }
    }

问题:async { block }中没有调用挂起函数(应该写block()),async内部只是返回了函数本身,导致Job立刻完成,所以result.isCompleted始终为true。此外,即使修复调用逻辑,不推进虚拟时间的话,runTest的调度器不会自动处理任务,Job状态也不会更新。


第二种方案

// 扩展函数
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
    async(block = block).run {
        this@assertNotCompleted.advanceTimeBy(5000L)
        getCompletionExceptionOrNull()
    }
}

// 使用示例
@Test(expected = IllegalStateException::class)
fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
    runTest {
        assertNotCompleted {
            someClass.waitForValue() 
        }
    }

问题:依赖expected注解捕获异常的设计不合理——测试中其他位置抛出的同类型异常会导致测试误通过,无法精准控制断言逻辑。


第三种方案

// 扩展函数
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
    runCatching {
        val result = async { 
            block
            // advanceTimeBy(5000L) 测试始终通过
        }
        // advanceTimeBy(5000L) 测试始终失败
        result.getCompletionExceptionOrNull()
    }.also { 
        assertThat(it.isFailure, equalTo(true))
    }
}

问题:同样存在未调用block()的错误,且推进时间的位置错误,导致断言结果与挂起函数的实际状态完全无关。


正确实现方案
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> TestScope.assertNotCompleted(block: suspend CoroutineScope.() -> T) {
    // 启动挂起函数,不等待其完成
    val job = launch { block() }
    // 推进虚拟时间,模拟等待5秒
    advanceTimeBy(5000L)
    // 确保调度器处理完所有待执行任务,避免状态检查滞后
    runCurrent()
    // 断言Job仍未完成,添加自定义错误信息方便调试
    assertThat(job.isCompleted)
        .overridingErrorMessage("挂起函数意外完成,预期会保持挂起状态")
        .isEqualTo(false)
    // 取消Job,防止测试资源泄漏
    job.cancel()
}

使用示例

@Test
fun `Given no value is ready, when waitForValue is called, then the suspendable function is not complete`() =
    runTest {
        assertNotCompleted {
            someClass.waitForValue() 
        }
    }

额外优化(可选)

如果需要确保挂起函数不会因异常结束,可以添加补充断言:

assertThat(job.isFailed)
    .overridingErrorMessage("挂起函数意外抛出异常")
    .isEqualTo(false)

内容的提问来源于stack exchange,提问作者James Olrog

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 10:13:21