测试含异步调用方法遇协程调度器及MockK报错,求排查方案
问题分析与修复
问题场景
我尝试为包含异步调用的openIntercom方法编写单元测试,遇到两个问题:
- 使用
runTest时抛出IllegalStateException: Module with the Main dispatcher had failed to initialize - 移除
runTest后抛出io.mockk.MockKException: no answer found for: ApiRepository(#1).openIntercomByJWT(123, continuation {})
目标方法代码
fun openIntercom(intercomId: String, onCompletable: (Boolean) -> Unit) { CoroutineScope(Dispatchers.IO).launch { try { apiRepository.openIntercomByJWT(intercomId.toInt()).apply { if (this != null && this.status.equals("ok")) { withContext(Dispatchers.Main) { onCompletable(true) } } else { withContext(Dispatchers.Main) { onCompletable(false) } } } } catch (e: Exception) { Timber.e(e, "[openIntercom] error") withContext(Dispatchers.Main) { onCompletable(false) } } } }
测试代码
@Test fun `openIntercom and return success`() = runTest{ val viewModel = IntercomMainViewModel(apiRepository) coEvery { apiRepository.openIntercomByJWT(any()) } returns StatusResponseData("ok") var result = false viewModel.openIntercom("123") { b -> result = b } Assert.assertEquals(true, result) }
已添加的测试规则与配置
class TestViewModelScopeRule( private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()) :TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() }}
testOptions { unitTests.returnDefaultValues = true }
错误原因与修复方案
1. Main Dispatcher初始化失败的问题
runTest会自动创建并管理TestDispatcher,但自定义的TestViewModelScopeRule又手动调用Dispatchers.setMain,两者的Dispatcher上下文未对齐,导致初始化异常。另外,目标方法里直接用CoroutineScope(Dispatchers.IO)创建独立协程,这个Scope未使用测试Dispatcher,测试无法控制其执行时机。
修复步骤:
- 移除自定义的
TestViewModelScopeRule,改用runTest自带的Dispatcher管理 - 替换目标方法里的自定义
CoroutineScope为ViewModel自带的viewModelScope,便于测试时控制协程生命周期
修改后的目标方法:
fun openIntercom(intercomId: String, onCompletable: (Boolean) -> Unit) { viewModelScope.launch(Dispatchers.IO) { // 使用viewModelScope而非自定义CoroutineScope try { apiRepository.openIntercomByJWT(intercomId.toInt()).apply { if (this != null && this.status.equals("ok")) { withContext(Dispatchers.Main) { onCompletable(true) } } else { withContext(Dispatchers.Main) { onCompletable(false) } } } } catch (e: Exception) { Timber.e(e, "[openIntercom] error") withContext(Dispatchers.Main) { onCompletable(false) } } } }
调整后的测试代码:
@Test fun `openIntercom and return success`() = runTest { // 绑定Main Dispatcher到测试用Dispatcher Dispatchers.setMain(testDispatcher) val viewModel = IntercomMainViewModel(apiRepository) coEvery { apiRepository.openIntercomByJWT(any()) } returns StatusResponseData("ok") var result = false viewModel.openIntercom("123") { b -> result = b } // 等待所有后台协程执行完成 advanceUntilIdle() Assert.assertEquals(true, result) // 重置Main Dispatcher Dispatchers.resetMain() }
2. No answer found的问题
移除runTest后,协程在后台异步执行,测试代码会直接走到断言,此时mock的挂起函数还未被调用;同时缺少测试Dispatcher的管理,协程无法正常触发执行逻辑。
修复步骤:
- 必须保留
runTest来管理协程测试,它会自动处理挂起函数的执行时机 - 调用
advanceUntilIdle()确保所有异步操作完成后再执行断言 - 确保
coEvery的匹配条件与实际调用参数一致(当前any()匹配123是正常的,若有类型不匹配需调整)
额外优化建议
- 永远优先使用
viewModelScope而非手动创建CoroutineScope,它会在ViewModel销毁时自动取消协程,避免内存泄漏 - 测试中用
advanceUntilIdle()或runCurrent()控制协程执行节奏,确保断言前异步逻辑已完成
内容的提问来源于stack exchange,提问作者ge0graff
相关产品推荐
相关产品推荐

