Kotlin单元测试:如何验证模拟等待后挂起函数未返回值
可行实现方案
直接使用kotlinx-coroutines-test提供的虚拟时间调度能力即可,不需要引入额外第三方依赖,也不会出现测试卡住、偶现误判的问题。
核心逻辑是利用runTest块的虚拟时钟特性:你可以指定任意长度的等待超时,测试执行时不会真的等待对应真实时长,调度器会直接跳转虚拟时间到超时点,检查挂起函数是否恢复。
修正后的第二个测试代码
@Test fun `waitForCompletionSignal when completionSignal is not provided assert no result`() = runTest { // 启动挂起函数执行 val result = async { waitForCompletionSignal() } // 用100年的虚拟时间窗口等待结果,真实执行耗时仅几毫秒 val executeResult = runCatching { withTimeout(Duration.parse("P36500D")) { result.await() } } // 校验:窗口内未拿到结果,触发超时,证明函数一直挂起未返回 assert(executeResult.exceptionOrNull() is TimeoutCancellationException) { "未发送完成信号时函数提前返回,返回值:${executeResult.getOrNull()}" } // 可选补充校验:发送信号后函数可正常返回,排除协程异常死锁的干扰 completionSignal.trySend(Unit) assert(result.await() == "Completion signal received") }
注:如果使用的协程测试库版本较低不支持
java.time.Duration参数,直接传入毫秒值即可,比如withTimeout(10000L * 365 * 24 * 60 * 60 * 1000)代表10000年虚拟时长,效果完全一致。注意导入kotlinx.coroutines.TimeoutCancellationException,不要误用JDK自带的超时异常类。
方案说明
- 解决等待时长判定问题:虚拟时间不占用真实执行时长,你可以把超时窗口设到任意长度,完全覆盖“函数永远等不到信号就永远不返回”的逻辑判定,不会拖慢测试速度。
- 解决测试稳定性问题:
- 协程调度逻辑由
runTest的测试调度器统一控制,不会出现真实环境下的线程调度延迟导致的偶现误判 - 如果后续代码变更导致函数不需要等信号就提前返回,
withTimeout块会立刻拿到返回值,不会触发超时,断言直接失败,不会出现测试卡住、漏判的情况
- 协程调度逻辑由
- 避坑提示:不要直接在启动
async后立刻判断result.isCompleted,此时协程可能还没调度到completionSignal.receive()挂起点,会出现偶发的状态误判。withTimeout的写法会自动处理协程调度,等协程真正进入挂起状态后再做时间跳转校验,结果稳定。
补充:第一个测试的小优化
你写的第一个测试逻辑是对的,但可以补一行调度确保协程真正进入挂起点再发信号,避免后续如果Channel改成无缓冲RENDEZVOUS类型时出现偶发问题:
@Test fun `waitForCompletionSignal when completionSignal is provided assert result`() = runTest { val result = async { waitForCompletionSignal() } // 推进调度,让协程执行到receive挂起点 advanceUntilIdle() completionSignal.trySend(Unit) assert(result.await() == "Completion signal received") }
内容的提问来源于stack exchange,提问作者Hylke
相关产品推荐
相关产品推荐

