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
相关产品推荐
相关产品推荐

