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

如何在测试中保证Kotlin协程执行顺序(不修改生产代码)

如何在不修改生产代码的情况下,测试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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 09:57:52