如何在kotlin.coroutines.test.runTest中使用kotlin.coroutines.withTi



我有一个suspend函数,它对外部API进行rest调用,我想在1分钟后超时。

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

我正在尝试用Junit5和kotlin.coroutines.test1.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$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)

kotlin.coroutines.test.runTest似乎在withTimeout上提前虚拟时间,而没有给它任何时间来执行它的主体。请参阅(https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md#using-运行时间内超时(

不幸的是,文档并没有提供绕过这一问题的方法。

请建议如何使用runTest测试此函数。

这是因为延迟跳过。

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

从这个调度器的角度来看,所有没有delay()的东西都会立即运行,而延迟的东西会使假时间进展。

然而,这不能用于测试调度程序之外真正需要实际时间的事情,因为测试不会真正等待。因此,从本质上讲,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
}
}

最新更新