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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 00:00:00