Tengo una función de suspensión que realiza una llamada de descanso a una API externa que quiero que se agote después de 1 minuto.
suspend fun makeApiCallWithTimeout(): List<ApiResponseData> = withTimeout(1.minutes) { apiCall() }Estoy tratando de probarlo con Junit5 y kotlinx.coroutines.test 1.6.0 así:
@Test fun `Test api call`() = runTest { val responseData = "[]" mockWebServer.enqueue(mockResponse(body = responseData) val result = sut.makeApiCallWithTimeout() advanceUntilIdle() assertEquals(0, result.size) }Desafortunadamente, recibo errores que se ven así:
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)Parece que kotlinx.coroutines.test.runTest está avanzando el tiempo virtual en withTimeout sin darle tiempo para ejecutar su cuerpo. Ver ( https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md#using-withtimeout-inside-runtest )
Desafortunadamente, la documentación no proporciona una forma de evitar esto.
Indique cómo probar esta función usando runTest.
Esto se debe a la omisión de retardo.
Aquí está usando runTest , que brinda capacidades de control de tiempo a su prueba. Para ello, este generador de corrutinas proporciona a un despachador una hora falsa que omite automáticamente los retrasos (desde la perspectiva del tiempo real), pero realiza un seguimiento interno de la hora falsa.
Desde el punto de vista de este despachador, todo lo que no tiene delay() s se ejecuta instantáneamente, mientras que las cosas que sí retrasan hacen que el tiempo falso progrese.
Sin embargo, esto no se puede usar con cosas que realmente toman tiempo real fuera del despachador de prueba, porque la prueba realmente no esperará. Entonces, en esencia, withTimeout se agota inmediatamente porque la apiCall() real probablemente se ejecuta fuera del despachador (y toma tiempo real).
Puede reproducir fácilmente este comportamiento de esta manera:
@Test fun test() = runTest { withTimeout(1000) { // immediately times out apiCall() } } suspend fun apiCall() = withContext(Dispatchers.IO) { Thread.sleep(100) // not even 1s }Normalmente hay 2 soluciones:
si desea seguir usando el tiempo controlado, debe asegurarse de estar usando el despachador de prueba en todo el código relevante. Esto significa que los lugares en su código donde usa ámbitos de corrutina personalizados o despachadores explícitos deberían permitir inyectar un despachador
si realmente no necesita tiempo controlado, puede usar runBlocking en lugar de runTest (en JVM) o seguir usando runTest pero ejecutar la prueba en otro despachador como Dispatchers.Default :
fun test() = runTest { withContext(Dispatchers.Default) { // test code } }Una adición a la respuesta de Joffrey.
Ejemplo de cómo inyectar el programador si desea seguir usando el tiempo controlado:
@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 } }