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

JUnit+Mockito测试API遇UncompletedCoroutinesError问题求助

JUnit+Mockito测试API响应码时遇到UncompletedCoroutinesError问题排查及疑问解答

错误现象

测试返回码104的接口时触发kotlinx.coroutines.test.UncompletedCoroutinesError,错误日志如下:

After waiting for 10s, the test coroutine is not completing
kotlinx.coroutines.test.UncompletedCoroutinesError: After waiting for 10s, the test coroutine is not completing
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$2$1.invoke(TestBuilders.kt:349)
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$2$1.invoke(TestBuilders.kt:333)
    at app//kotlinx.coroutines.InvokeOnCancelling.invoke(JobSupport.kt:1430)
    at app//kotlinx.coroutines.JobSupport.notifyCancelling(JobSupport.kt:1474)
    at app//kotlinx.coroutines.JobSupport.tryMakeCancelling(JobSupport.kt:798)
    at app//kotlinx.coroutines.JobSupport.makeCancelling(JobSupport.kt:758)
    at app//kotlinx.coroutines.JobSupport.cancelImpl$kotlinx_coroutines_core(JobSupport.kt:674)
    at app//kotlinx.coroutines.JobSupport.cancelCoroutine(JobSupport.kt:661)
    at app//kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:159)
    at app//kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:501)
    at app//kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:280)
    at app//kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:108)
    at java.base@17.0.6/java.lang.Thread.run(Unknown Source)

相关代码

MainCoroutineRule实现

@ExperimentalCoroutinesApi
class MainCoroutineRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {

    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

API测试类代码

@ExperimentalCoroutinesApi
class ApiServicesTest {
    @get:Rule
    val mockitoRule: MockitoRule = MockitoJUnit.rule()

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    private lateinit var mockWebServer: MockWebServer
    private lateinit var apiServices: ApiServices

    @Before
    fun setup() {
        mockWebServer = MockWebServer()
        apiServices = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create()
    }

    @After
    fun teardown() {
        mockWebServer.shutdown()
    }

    @Test
    fun `test1`() =
        runTest {
            mockWebServer.enqueue(
                MockResponse()
                    .setResponseCode(200)
                    .setBody("{}")
            )

            val response = apiServices.getLatestRates("", "")
            val body = response.body()
            assertThat(body).isNotNull()
        }


    @Test
    fun `t2`() = runTest {
            mockWebServer.enqueue(MockResponse()
                .setResponseCode(104)
                .setBody("{}")
            )
            val apiCall = apiServices.getLatestRates("", "")
            assertThat(apiCall.code()).isEqualTo(104)
        }
}

已尝试操作

  • 将UnconfinedTestDispatcher切换为标准Dispatcher,问题未解决
  • 测试返回码200、404的接口时,测试均正常运行

问题解答

1. 为什么返回码104会触发UncompletedCoroutinesError?

HTTP 104属于信息响应状态码(1xx),这类响应的规范定义是仅返回状态头,不携带响应体——即使你手动设置了setBody("{}"),Retrofit在处理时仍会因为尝试解析不存在的响应体而挂起协程,最终导致测试超时抛出错误。

解决方法:

  • 针对1xx响应,不要设置响应体,且测试时仅校验状态码,跳过响应体解析:
@Test
fun `t2`() = runTest {
    mockWebServer.enqueue(MockResponse()
        .setResponseCode(104)
        // 移除setBody,符合1xx响应规范
    )
    val apiCall = apiServices.getLatestRates("", "")
    assertThat(apiCall.code()).isEqualTo(104)
    // 不要调用body()方法
}
  • 如果接口确实可能返回1xx响应,建议将Retrofit接口方法的返回类型改为Response<Void>,明确表示该响应无体。

2. 当前API测试是否足够覆盖需求?

当前测试仅覆盖了有限场景,还缺少以下关键测试点:

  • 客户端错误场景:400参数错误、401未授权、403禁止访问、404资源不存在等
  • 服务器错误场景:500内部错误、503服务不可用等
  • 异常场景:网络超时、连接失败、响应体格式错误(如返回非JSON内容)
  • 边界场景:空参数、特殊字符参数、超大响应体等

补充上述场景的测试后,才能更全面覆盖接口测试需求。

3. 返回码小于200是否被禁止?

HTTP规范中,1xx(小于200)属于信息响应码,并非被禁止——这类码用于HTTP层的交互通知(如100 Continue表示服务器已接收请求头,可继续发送请求体)。只是大部分业务接口很少用到这类码,因为它们不属于业务逻辑返回的结果。

你测试时出现问题,是因为Retrofit处理无响应体的1xx响应时,和代码中尝试解析响应体的逻辑冲突导致的,并非1xx本身被禁止。

内容的提问来源于stack exchange,提问作者SlipperyBarrel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 23:30:34