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

Jol*_*boy 18 kotlin kotlin-coroutines

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

suspend fun makeApiCallWithTimeout(): List<ApiResponseData> =
   withTimeout(1.minutes) {
      apiCall()
   }
        
Run Code Online (Sandbox Code Playgroud)

我正在尝试使用 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)
}
Run Code Online (Sandbox Code Playgroud)

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

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$1$1.invokeSuspend(TestBuilders.kt:167)
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
    at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$1$1.invoke(TestBuilders.kt)
    at app//kotlinx.coroutines.test.TestBuildersJvmKt$createTestResult$1.invokeSuspend(TestBuildersJvm.kt:13)
    (Coroutine boundary)
Run Code Online (Sandbox Code Playgroud)

看起来 kotlinx.coroutines.test.runTest 正在提前 withTimeout 上的虚拟时间,而没有给它任何时间来执行其主体。请参阅(https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md#using-withtimeout-inside-runtest)

不幸的是,文档没有提供解决这个问题的方法。

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

Jof*_*rey 12

这是因为延迟跳过。

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

从这个调度程序的角度来看,所有没有delay()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
}
Run Code Online (Sandbox Code Playgroud)

通常有2种解决方案:

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

  • 如果您确实不需要受控时间,您可以使用(在 JVM 上)runBlocking代替或继续使用但在另一个调度程序上运行测试,例如:runTestrunTestDispatchers.Default

fun test() = runTest {
    withContext(Dispatchers.Default) {
        // test code
    }
}
Run Code Online (Sandbox Code Playgroud)