Kotlin协程结合Retrofit2错误处理单元测试执行顺序异常问题
问题根源
interactor.load中启动的协程本身是异步执行的,虽然你已经通过MainCoroutineRule接管了 Main 调度器,但没有主动等待所有协程任务执行完成就直接运行了断言逻辑。- 成功场景可以正常通过属于测试环境的偶发现象:MockWebServer返回200响应时,Retrofit的序列化+回调链路耗时极短,刚好在断言执行前完成了全链路操作;而错误响应的处理链路多了异常实例创建、调用栈采集等步骤,耗时略长,就出现了断言先执行的问题。
- 之前尝试
runBlocking/runBlockingTest无效的原因是:这两个函数只会等待自身内部启动的协程执行完成,不会等待viewModelScope中启动的独立协程。
解决方案
方案1:通过MainCoroutineRule主动等待所有任务执行完成
你使用的MainCoroutineRule提供了advanceUntilIdle()方法,可以阻塞测试线程,直到调度器中所有排队的任务全部执行完成,直接修改测试代码即可:
@Test fun viewModel_loadData_correctErrorHandling() = runTest { mockServer.enqueue(MockResponse().apply { setResponseCode(500) }) viewModel.loadModel() // 新增这行,等待所有协程任务执行完毕再执行断言 mainCoroutineRule.dispatcher.advanceUntilIdle() assert(!viewModel.loading) assert(viewModel.loadingVisibility.value != View.VISIBLE) assertNotNull(viewModel.currentError) assert(viewModel.errorVisibility.value == View.VISIBLE) assertNull(viewModel.model.value) assert(viewModel.contentVisibility.value != View.VISIBLE) }
如果使用的是旧版本协程测试库,将runTest替换为runBlockingTest即可。
方案2:调整代码结构返回协程Job方便等待
如果不想依赖测试规则的调度器能力,可以修改业务代码返回协程Job对象,测试时直接等待Job执行完成:
// 调整ViewModel的loadModel方法,返回协程Job fun loadModel(): Job { currentError = null loading = true model.value = null return interactor.load(viewModelScope, @MainThread { loading = false model.value = it }, @MainThread { loading = false currentError = it Timber.e(it) }) } // 调整interactor的load方法,返回启动的Job fun load( scope: CoroutineScope, onSuccess: (List<ConsumableCategory>) -> Unit, onError: (Throwable) -> Unit ): Job { return scope.launch { try { onSuccess(dataManager.getConsumableCategories()) } catch (t: Throwable) { onError(t) } } }
测试时调用join()等待协程执行完成:
@Test fun viewModel_loadData_correctErrorHandling() = runTest { mockServer.enqueue(MockResponse().apply { setResponseCode(500) }) val loadJob = viewModel.loadModel() loadJob.join() // 等待协程执行完毕再执行断言 // 后续断言逻辑保持不变 }
额外检查项
确认你的MainCoroutineRule实现正确,确实替换了全局Main调度器:
@ExperimentalCoroutinesApi class MainCoroutineRule(val dispatcher: TestDispatcher = StandardTestDispatcher()) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(dispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() } }
内容的提问来源于stack exchange,提问作者Kent Hawkings
相关产品推荐
相关产品推荐

