如何在kotlinx.coroutines.test.runTest中使用kotlinx.coroutines.withTimeout?

How to use kotlinx.coroutines.withTimeout in kotlinx.coroutines.test.runTest?

我有一个挂起函数,可以对外部 API 进行休息调用,我想在 1 分钟后超时。

suspend fun makeApiCallWithTimeout(): List<ApiResponseData> =
   withTimeout(1.minutes) {
      apiCall()
   }
        

我正在尝试使用 Junit5 和 kotlinx.coroutines.test 1.6.0 进行测试,如下所示:

@Test
fun `Test api call`() = runTest {
   val responseData = "[]"
   mockWebServer.enqueue(mockResponse(body = responseData)
   val result = sut.makeApiCallWithTimeout()
   advanceUntilIdle()
   assertEquals(0, result.size)
}

不幸的是,我收到如下所示的错误:

Timed out waiting for 60000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 60000 ms
    at app//kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
    at app//kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
    at app//kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:23)
    at app//kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTask(TestCoroutineScheduler.kt:95)
    at app//kotlinx.coroutines.test.TestCoroutineScheduler.advanceUntilIdle(TestCoroutineScheduler.kt:110)
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTestCoroutine(TestBuilders.kt:212)
    at app//kotlinx.coroutines.test.TestBuildersKt.runTestCoroutine(Unknown Source)
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invokeSuspend(TestBuilders.kt:167)
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest.invoke(TestBuilders.kt)
    at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult.invokeSuspend(TestBuildersJvm.kt:13)
    (Coroutine boundary)

kotlinx.coroutines.test.runTest 似乎在 withTimeout 上提前了虚拟时间,却没有给它任何时间来执行它的主体。参见 (https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md#using-withtimeout-inside-runtest)

遗憾的是,文档没有提供解决此问题的方法。

请告知如何使用 runTest 测试此功能。

这是因为延迟跳过。

这里您使用的是 runTest,它为您的测试带来了时间控制功能。为此,这个协程构建器为调度程序提供了一个自动跳过延迟(从实时角度来看)的假时间,但在内部跟踪假时间。

从这个调度员的角度来看,所有没有 delay()s 运行s 的东西都是即时的,而延迟的东西会让假时间进步。

但是,这不能用于在测试调度程序之外真正需要实际时间的事情,因为测试不会真正等待。所以本质上,withTimeout 立即超时,因为实际的 apiCall() 可能 运行 在调度程序之外(并且需要实时)。

您可以像这样轻松重现此行为:

@Test
fun test() = runTest {
    withTimeout(1000) { // immediately times out
        apiCall()
    }
}

suspend fun apiCall() = withContext(Dispatchers.IO) {
    Thread.sleep(100) // not even 1s
}

通常有两种解决方案:

  • 如果您想继续使用受控时间,则必须确保在所有相关代码中都使用了测试调度程序。这意味着代码中使用自定义协程范围或显式调度程序的地方应该允许注入调度程序

  • 如果你真的不需要控制时间,你可以使用 runBlocking 而不是 runTest (在 JVM 上)或者继续使用 runTest 但 运行 对另一个调度程序的测试,例如 Dispatchers.Default:

fun test() = runTest {
    withContext(Dispatchers.Default) {
        // test code
    }
}

Joffrey 回答的补充

如果您想继续使用受控时间,则注入调度程序的示例:

@Test
fun test() = runTest {
    val gw = MyGateway(testScheduler)
    withTimeout(1000) {
        gw.apiCall()
    }
}

class MyGateway(private val context: CoroutineContext = Dispatchers.IO) {
    companion object : Logging
    suspend fun apiCall() = withContext(context) {
        Thread.sleep(100) // not even 1s
    }
}