如何在测试中保证Kotlin协程执行顺序(不修改生产代码)
问题背景
我有一段基于Kotlin结构化并发实现的业务逻辑,每个任务会通过Web客户端发起HTTP请求:
suspend fun logic(): Something = coroutineScope { launch { taskOne() } launch { taskTwo() } launch { taskThree() } launch { taskFour() } Something(...) }
测试阶段使用MockWebServer模拟响应:
val server = MockWebServer() @Test fun logicTest() { server.enqueue(MockResponse().setResponseCode(200).setBody(read("one.json"))) server.enqueue(MockResponse().setResponseCode(200).setBody(read("two.json"))) server.enqueue(MockResponse().setResponseCode(200).setBody(read("three.json"))) server.enqueue(MockResponse().setResponseCode(200).setBody(read("four.json"))) val result = runBlocking { logic() } result shouldBe Something(...) }
运行全部30个测试时,协程偶尔会出现执行顺序错乱的情况——生产环境下这完全没问题,但MockWebServer是按队列顺序返回响应的,一旦taskThree先执行,就会拿到原本给taskTwo的响应,直接导致测试抛出异常。
我需要在测试中保证协程的执行顺序,但绝对不能修改生产代码(生产环境中顺序无关),因此无法使用Mutex、Channel这类同步手段。我已经尝试过runTest、runBlocking(Dispatchers.Default.limitedParallelism(1))以及runBlocking(Executors.newSingleThreadExecutor().asCoroutineDispatcher()),均未解决问题。
目前有两个临时可行方案:使用MockWebServer的Dispatcher匹配请求,或者用MockResponse.throttleBody()延迟响应,但我想寻求更直接的解决方案。
解决方案
1. 使用StandardTestDispatcher强制协程串行执行(推荐)
kotlinx-coroutines-test库中的StandardTestDispatcher会让所有协程按提交顺序串行执行,完美匹配MockWebServer的队列响应逻辑,且无需修改生产代码。
只需在测试中用StandardTestDispatcher调用runTest即可:
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest @Test fun logicTest() = runTest(StandardTestDispatcher()) { server.enqueue(MockResponse().setResponseCode(200).setBody(read("one.json"))) server.enqueue(MockResponse().setResponseCode(200).setBody(read("two.json"))) server.enqueue(MockResponse().setResponseCode(200).setBody(read("three.json"))) server.enqueue(MockResponse().setResponseCode(200).setBody(read("four.json"))) val result = logic() result shouldBe Something(...) }
原理:StandardTestDispatcher会把所有协程任务放入一个队列,按启动顺序依次执行,确保taskOne()到taskFour()按代码中的launch顺序发起请求,完全匹配MockWebServer的响应队列。
2. 替换HTTP客户端的线程池为单线程(若客户端自带线程池)
如果你的HTTP客户端(比如OkHttp)有自己的线程池,即使协程用了单线程调度器,客户端仍可能并行发起请求。这时需要配置客户端的线程池为单线程:
// 测试中初始化OkHttpClient val okHttpClient = OkHttpClient.Builder() .dispatcher(Dispatcher(Executors.newSingleThreadExecutor())) .build()
将这个单线程的客户端注入到生产代码的Web客户端实例中(如果生产代码支持依赖注入),就能保证请求串行发起,匹配MockWebServer的队列顺序。
为什么之前的单线程调度器尝试失败?
你之前用limitedParallelism(1)或自定义单线程调度器无效,核心原因是:
launch默认继承coroutineScope的上下文,但如果taskOne()等内部切换了调度器(比如HTTP客户端用Dispatchers.IO),单线程调度器只会限制协程的启动阶段,实际请求仍会在客户端的线程池并行执行。StandardTestDispatcher会接管所有协程的调度,包括内部切换的调度器(只要用的是协程上下文的调度器),而自定义单线程调度器不会自动替换其他默认调度器。
内容的提问来源于stack exchange,提问作者Adel Khial

