Kotlin协程测试遇UncompletedCoroutinesError问题排查求助
问题:Kotlin协程测试触发UncompletedCoroutinesError
错误详情
报错信息:
kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 1m, the test coroutine is not completing
该错误出现在KotlinExampleTests类的测试用例Test whether the Example widget icons are displayed in zero state中。
测试代码片段
@Test fun `Test whether the Example widget icons are displayed in zero state`() = runTestSafe { val dispatcher = StandardTestDispatcher(testScheduler) val viewModel = createViewModel(dispatcher, this) viewModel.responseFlow.value = mockResponseFlow() composeTestRule.setContent { ExampleWidget( Modifier, mockZeroState(), { _, _-> }, { _, _ -> }, { _, _ -> }, { _ -> }, { _-> } ) } composeTestRule.onNodeWithTag("Tag1").assertExists() composeTestRule.onNodeWithTag("Tag2").assertExists() composeTestRule.onNodeWithTag("Tag3").assertExists() assert(0 == viewModel.mockResponseFlow.value?.icons?.size) }
已尝试操作
- 增大
runTest超时时间至1分钟 - 验证测试中启动的协程在测试结束时已被正确取消
现象
- 单独运行该测试套件时通过
- 运行全部测试套件时失败
原因分析
- 测试间状态泄漏:全量测试运行时,前序测试的协程、Dispatcher状态或Compose组件状态未被完全清理,导致当前测试的协程被阻塞。
- TestDispatcher未正确隔离:
StandardTestDispatcher如果和其他测试共享testScheduler,可能导致调度任务交叉干扰,使得当前测试的协程无法完成。 - Compose测试的协程未同步:
setContent中可能存在未被composeTestRule管控的协程,或者UI断言后未等待Compose帧完成,导致协程残留。 - ViewModel协程作用域问题:
createViewModel传入的作用域如果未正确绑定测试的runTest作用域,可能导致ViewModel中的协程脱离测试管控,无法被及时取消。
排查与修复方案
排查步骤
- 检查
createViewModel方法的实现,确认传入的this(测试作用域)是否被正确用作ViewModel的CoroutineScope,ViewModel是否在测试结束时被正确销毁。 - 查看前序测试是否有未清理的协程或Dispatcher状态,比如是否共享了TestDispatcher实例。
- 在测试的关键节点(如断言后)添加
composeTestRule.waitForIdle(),观察是否能解决问题,验证是否是Compose协程未同步导致。
修复方案
- 隔离TestDispatcher:每个测试用例创建独立的
StandardTestDispatcher,不共享testScheduler,避免测试间的调度干扰:
@Test fun `Test whether the Example widget icons are displayed in zero state`() = runTest { val testDispatcher = StandardTestDispatcher() val viewModel = createViewModel(testDispatcher, this) // ... 其余代码 }
- 强制同步Compose状态:在UI断言后添加
waitForIdle(),确保Compose的所有协程任务完成:
composeTestRule.onNodeWithTag("Tag3").assertExists() composeTestRule.waitForIdle() // 等待Compose完成所有UI渲染任务 assert(0 == viewModel.mockResponseFlow.value?.icons?.size)
- 确保ViewModel作用域绑定测试Scope:检查
createViewModel的实现,确保ViewModel的CoroutineScope使用测试传入的作用域,或者在测试结束时手动取消ViewModel的作用域:
// 示例createViewModel实现 fun createViewModel(dispatcher: Dispatcher, scope: CoroutineScope): ExampleViewModel { return ExampleViewModel(scope + dispatcher) }
- 添加测试后清理逻辑:在
@AfterEach方法中清理Compose状态和ViewModel资源,避免状态泄漏:
@AfterEach fun tearDown() { composeTestRule.clearContent() // 如果有需要,手动取消ViewModel的协程作用域 }
- 替换runTestSafe为标准runTest:如果
runTestSafe是自定义封装,可能存在协程管控的问题,改用官方kotlinx.coroutines.test.runTest,确保协程的自动取消和管控。
内容的提问来源于stack exchange,提问作者Kotiswar Kaithepalli
相关产品推荐
相关产品推荐

