Saltar al contenido
K
Corrutinas y FlowPaso a paso33 min de lectura · 8 min de máquina Básico

Tests de corrutinas: tiempo virtual

Al terminar: Probar corrutinas con runTest, tiempo virtual e inyectando el dispatcher.

Al terminar esta lección Marcador probará un partido de 90 minutos en unas decenas de milisegundos, sin esperar de verdad y sin que el resultado dependa de lo rápida que sea tu máquina. Y sabrás por qué los tests de las lecciones anteriores, que sí esperan, son frágiles.

Es la penúltima pieza del módulo: sin tests que corran deprisa, nadie se atreve a tocar código con corrutinas. Además, es la primera vez que instalas una librería solo para los tests: kotlinx-coroutines-test. Temas: runTest, tiempo virtual, TestDispatcher, inyectar el dispatcher, y la diferencia entre implementation y testImplementation.

El problema: los tests que esperan de verdad

Hasta ahora las pruebas de corrutinas usan runBlocking y un partido acelerado: unos pocos ms por minuto de juego en lugar de un minuto real. Funciona, pero tiene tres costes:

  • Esperan de verdad. Un partido a tiempo real son 5.400.000 ms, es decir, 90 minutos. Acelerarlo lo hace tolerable, pero cada test sigue durmiendo.
  • Dependen de la máquina. Hay aserciones como ms < 1500 o ms < 250 (un tiempo real medido, con margen amplio). En un ordenador lento o en un servidor saturado, un test correcto puede fallar.
  • No son deterministas. Si dos corrutinas se pisan por milisegundos, el test pasa unas veces y otras no. Esos tests son los que acaban ignorándose.

El tiempo virtual quita los tres problemas de golpe.

Tiempo virtual: runTest

runTest crea un alcance de test (se llama TestScope) con un planificador (testScheduler) que lleva un reloj propio. Cuando lo único que queda por hacer es esperar un delay, el planificador salta directamente al momento en que acabaría. currentTime lee ese reloj.

Pruébalo en el Kotlin Playground, que trae la librería de tests de corrutinas y deja usar runTest dentro de un main:

ejemplo mínimo: un reloj virtual (con comentarios)
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
 
@OptIn(ExperimentalCoroutinesApi::class)   // currentTime es experimental
fun main() = runTest {                     // runTest: un reloj virtual
    delay(90 * 60_000L)                    // 90 minutos… que no se esperan
    println("Reloj virtual: $currentTime ms")   // el reloj virtual sí avanzó
}
Reloj virtual: 5400000 ms

Se ejecuta en un instante y el reloj virtual marca 5.400.000 ms: nada durmió.

Cuando saltarse todo no es lo que quieres, hay tres funciones para gobernar el reloj. Las usarás en el ejercicio de abajo y en la lección siguiente:

FunciónQué hace
advanceTimeBy(ms)Adelanta el reloj ms y ejecuta lo que toca antes de ese instante
runCurrent()Ejecuta lo que está pendiente en este instante, sin mover el reloj
advanceUntilIdle()Adelanta hasta que no quede trabajo pendiente

Por eso, después de adelantar, se llama a runCurrent(): ejecuta lo que cae justo en el límite.

Las tres, con una corrutina lanzada dentro del test. launch { … } dentro de runTest crea una hija del test: corre dentro de él y runTest no termina hasta que ella acaba. Un panadero que avisa cuando el pan está listo:

ejemplo mínimo: gobernar el reloj a mano (con comentarios)
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
 
@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runTest {
    launch {                                          // hija del test: espera 1 s y avisa
        delay(1_000)
        println("Pan listo a los $currentTime ms")
    }
    advanceTimeBy(1_000)                              // llega al segundo 1, pero NO ejecuta lo que cae justo ahí
    println("Tras advanceTimeBy: aún no ha avisado")
    runCurrent()                                      // ejecuta lo de este instante: sale el aviso
    launch { delay(5_000); println("Segunda hornada a los $currentTime ms") }
    advanceUntilIdle()                                // adelanta hasta que no quede nada pendiente
    println("Reloj virtual: $currentTime ms")
}
Tras advanceTimeBy: aún no ha avisado
Pan listo a los 1000 ms
Segunda hornada a los 6000 ms
Reloj virtual: 6000 ms

Dispatchers de prueba, e inyectarlos

runTest sabe saltarse los delay de su planificador. Si tu código elige su propio dispatcher, como flowOn(Dispatchers.Default), esa corrutina corre en un hilo real y el reloj virtual no la alcanza.

Un aviso sobre lo que esto significa: en un test, todo corre en el único hilo del test, aunque en la app haya un grupo de hilos (Default). Un test con dispatchers de prueba no detecta problemas de paralelismo real; sirve para probar la lógica y el orden, no las carreras entre hilos.

La solución es inyectar el dispatcher: recibirlo de fuera, con un valor por defecto para la app. En producción es IO (o Default); en un test, uno de prueba que comparte el planificador de runTest:

ejemplo mínimo: reloj virtual y dispatcher inyectado (con comentarios)
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
 
// Código «de la app»: el dispatcher llega de fuera, con IO por defecto
class Biblioteca(private val dispatcher: CoroutineDispatcher = Dispatchers.IO) {
    suspend fun buscarLibro(): String = withContext(dispatcher) {
        delay(1_000)                       // una búsqueda que tarda 1 segundo
        "El Quijote"
    }
}
 
@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runTest {
    val biblioteca = Biblioteca(StandardTestDispatcher(testScheduler))  // el dispatcher de prueba, con el mismo reloj
    println("Encontrado: ${biblioteca.buscarLibro()}")
    println("Reloj virtual: $currentTime ms")
}
Encontrado: El Quijote
Reloj virtual: 1000 ms

StandardTestDispatcher(testScheduler) es un dispatcher de prueba: sus corrutinas esperan en cola hasta que el test se queda esperando (llega a un delay, un join()…) o llama a runCurrent(), y usan el reloj virtual del test. Hay un segundo, UnconfinedTestDispatcher, que arranca las corrutinas al instante; lo verás en la lección siguiente.

Antes del primer paso, a practicar el reloj virtual.

Reto · Ejercicio: cuenta atrás en tiempo virtual

En el Kotlin Playground, sin tocar tu proyecto: el Playground trae la librería de tests de corrutinas, y runTest también sirve dentro de un main.

  1. Escribe suspend fun cuentaAtras(desde: Int, alSegundo: (Int) -> Unit): un for (s in desde downTo 1) que espera delay(1_000) y llama a alSegundo(s).
  2. En fun main() = runTest { … } (con @OptIn(ExperimentalCoroutinesApi::class)), crea una lista mutable vistos y lanza con launch { … } la cuenta atrás desde 60, que apunte cada número en vistos.
  3. Usa advanceTimeBy(30_000) e imprime cuántos números van (vistos.size); runCurrent() e imprime otra vez; advanceUntilIdle() e imprime cuántos van, el último número (vistos.last()) y el reloj virtual (currentTime).

La consola debe mostrar:

Tras adelantar 30 s: 29
Tras runCurrent: 30
Al final: 60, último: 1
Reloj virtual: 60000 ms

Antes de ejecutar, predice los números que imprimirá.

Ver solución
solución: cuenta atrás en tiempo virtual
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
 
// Espera 1 s y avisa con cada número, de `desde` a 1.
suspend fun cuentaAtras(desde: Int, alSegundo: (Int) -> Unit) {
    for (s in desde downTo 1) {
        delay(1_000)
        alSegundo(s)
    }
}
 
@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runTest {
    val vistos = mutableListOf<Int>()
    launch { cuentaAtras(60) { vistos += it } }                   // hija normal del test
 
    advanceTimeBy(30_000)                                         // adelanta 30 s virtuales
    println("Tras adelantar 30 s: ${vistos.size}")
    runCurrent()                                                  // ejecuta lo que toca justo ahora
    println("Tras runCurrent: ${vistos.size}")
 
    advanceUntilIdle()                                            // adelanta hasta que no quede nada
    println("Al final: ${vistos.size}, último: ${vistos.last()}")
    println("Reloj virtual: $currentTime ms")
}

Tras advanceTimeBy(30_000) faltan las tareas programadas exactamente en el segundo 30 (la trigésima marca de la cuenta atrás): advanceTimeBy no las ejecuta. runCurrent() sí. Es el mismo límite que verás en la prueba del gol del minuto 12 de la lección siguiente. Y advanceUntilIdle() adelanta hasta el final porque aquí la cuenta atrás es hija normal del test, que sí espera por ella.

Paso · Instala la librería de test

Objetivo: que los tests de :torneo vean kotlinx-coroutines-test, y el código normal no.

Gradle Scripts
libs.versions.toml (Version Catalog "libs")se modifica: [libraries]
build.gradle.kts (Module :torneo)se modifica: dependencies
Los dos ficheros de esta instalación, tal como los ves en la vista Android.

1 · Qué instalas. kotlinx-coroutines-test (org.jetbrains.kotlinx, de la familia de Corrutinas y suspend) trae runTest, StandardTestDispatcher, backgroundScope, advanceTimeBy, currentTime: el reloj virtual.

2 · Programa y tests. :torneo tiene dos conjuntos de código que Gradle compila por separado: el del programa (src/main) y el de los tests (src/test, el paquete con (test)). La palabra de delante en dependencies decide qué conjunto ve la librería:

Las dos palabras de dependencies (con comentarios)
dependencies {
    implementation(libs.kotlinx.coroutines.core)    // la ven el programa y los tests, y va en el programa final
    testImplementation(libs.kotlin.test)            // solo los tests; no engorda lo que se entrega
}

3 · [versions]. No tocas esta sección: kotlinx-coroutines-test reutiliza kotlinxCoroutinesCore (mismo número, suben a la vez).

4 · [libraries]. Al final de la sección, una línea:

catálogo: línea nueva en [libraries]
# Reutiliza la versión de las corrutinas
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesCore" }

5 · Sin plugin. No tocas [plugins] ni la raíz.

6 · La dependencia. En build.gradle.kts (Module :torneo), debajo de la de kotlin-test:

build.gradle.kts (Module :torneo): el bloque dependencies, antes
dependencies {
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.kotlinx.coroutines.core)
    testImplementation(libs.kotlin.test)
}
build.gradle.kts (Module :torneo): el bloque dependencies, después (con comentarios)
dependencies {
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.kotlinx.coroutines.core)
    testImplementation(libs.kotlin.test)
    testImplementation(libs.kotlinx.coroutines.test)   // testImplementation: solo de tests
}

7 · Sync Now. Sin errores rojos; la primera vez necesita internet.

8 · Comprueba. En la Terminal de Android Studio (View → Tool Windows → Terminal). Lo que ve el programa:

./gradlew :torneo:dependencies --configuration runtimeClasspath

No debe nombrar kotlinx-coroutines-test. Lo que ven los tests:

./gradlew :torneo:dependencies --configuration testRuntimeClasspath

Debe aparecer org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0 (para no leer cientos de líneas, añade | grep coroutines-test al final del comando: | pasa la salida a grep, que deja solo las líneas que contienen ese texto). Es la diferencia entre implementation y testImplementation, medida.

Paso · Un partido de 90 minutos en milisegundos

Objetivo. Escribir tu primer test con runTest y verlo fallar tras un minuto; hacer que los dispatchers se inyecten en el código que ya existe; y ver pasar el mismo test en milisegundos.

torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo
PartidoEnVivo.ktse modifica
Simulacion.ktse modifica
Cronometro.ktse modifica
SegundoPlano.ktse modifica
torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo (test)
TiempoVirtualTest.ktse crea
Un test nuevo y cuatro ficheros modificados, tal como los ves en la vista Android.

Primero, un test que falla

Crea TiempoVirtualTest.kt. En torneo → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (test) (el que lleva (test)) → New → Kotlin Class/File → File → escribe TiempoVirtualTest y pulsa Intro. Se abre con solo la línea package. Escribe esto, con las dos librerías ya instaladas:

TiempoVirtualTest.kt: primer intento, sin inyectar el dispatcher
package com.sebastiannarvaez.marcador.torneo
 
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
 
@OptIn(ExperimentalCoroutinesApi::class)
class TiempoVirtualTest {
    private val guion = Ejemplo.rayoContraToros
    private val minuto = 60_000L // un minuto de juego = un minuto REAL
 
    @Test
    fun unPartidoDe90MinutosSeJuegaEnMilisegundos() = runTest {
        val final = jugarPartido(guion, msPorMinuto = minuto)
        assertEquals(2, final.golesLocal)
        assertEquals(90 * minuto, currentTime)
    }
}

Qué es cada cosa:

  • @OptIn(ExperimentalCoroutinesApi::class): aceptas usar currentTime, que está marcado como experimental (también lo están advanceTimeBy y runCurrent, que usarás en la lección siguiente), con el mismo @OptIn(ExperimentalCoroutinesApi::class) que pusiste sobre seguirPartido en Marcador.kt.
  • = runTest { … }: el cuerpo del test corre en el reloj virtual. Sin runTest (o runBlocking, que ya conoces), una función suspend como jugarPartido no se puede llamar desde un test, que es una función normal.
  • msPorMinuto = minuto: hace que cada minuto de juego dure un minuto de verdad (60.000 ms): 90 minutos son 5.400.000 ms.
  • currentTime: es el reloj virtual; si el partido «dura» 90 minutos de reloj virtual, vale 5_400_000.

Ejecútalo desde la Terminal de Android Studio, solo esta clase de tests:

./gradlew :torneo:test --tests "*TiempoVirtualTest*"

El --tests limita la ejecución a lo que coincida con el texto, y cada * significa «cualquier texto, o ninguno»: así «*TiempoVirtualTest*» vale aunque delante lleve el paquete.

Tardará un minuto en fallar, es normal. Acaba con algo así:

TiempoVirtualTest > unPartidoDe90MinutosSeJuegaEnMilisegundos() FAILED
    kotlinx.coroutines.test.UncompletedCoroutinesError at TestBuilders.kt:353

Después, inyectar el dispatcher

Cada función o clase que elige un dispatcher lo recibirá de fuera, con un valor por defecto igual al que tenía, para que cualquier llamada anterior siga compilando. Lo importante es que el dispatcher del test llegue hasta flowOn, withContext y CoroutineScope(...). En SegundoPlano.kt el cambio no lo usa ningún test nuevo: se hace por coherencia, para poder probar esas funciones igual en el futuro.

Piezas nuevas de este paso (las necesitas para escribirlo):

  • CoroutineDispatcher es el tipo de cualquier dispatcher (Default, IO, el de prueba…): lo que recibe el parámetro.
  • PartidoEnVivo(guion, msPorMinuto, dispatcher), jugarPartido(partido, msPorMinuto, dispatcher), jugarConCronometro(partido, msPorMinuto, dispatcher, alMinuto) y Transmisor(alFallar, dispatcher, jugar). El dispatcher va antes de las lambdas (alMinuto, jugar) por dos razones distintas. La primera: una lambda que va la última se puede escribir fuera de los paréntesis, como en jugarConCronometro(guion, minuto, dispatcher) { minutos += it }. La segunda, solo en Transmisor: el valor por defecto de jugar usa dispatcher, y un valor por defecto solo puede usar parámetros declarados antes que él.
  • StandardTestDispatcher(testScheduler) crea un dispatcher de prueba que comparte el reloj virtual de runTest.

Intenta escribirlo tú, fichero a fichero: PartidoEnVivo, jugarPartido, jugarConCronometro, el Transmisor y las dos funciones de SegundoPlano.kt que ya elegían dispatcher, guardarEnSegundoPlano y tablaEnSegundoPlano (la de cargar se queda como está), reciben el dispatcher que hoy eligen por su cuenta, con ese mismo como valor por defecto, y lo usan en el sitio donde antes lo escribían. Deja que Android Studio te ofrezca los import con Option+Enter sobre cada nombre en rojo. No hay salida nueva: ./gradlew :torneo:run imprime exactamente lo mismo que antes, y todo debe compilar. Cuando termines, abre el bloque y compara.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código

En PartidoEnVivo.kt se añade el import kotlinx.coroutines.CoroutineDispatcher, se cambia el encabezado de la clase y flowOn usa el parámetro. El resto del fichero no cambia.

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/PartidoEnVivo.kt
import kotlinx.coroutines.CoroutineDispatcher
 
    …
 
// `dispatcher` se INYECTA (llega de fuera con un valor por defecto): en la app
// es Default, pero una prueba con `runTest` le pasa su propio dispatcher de
// pruebas. Con `flowOn(Dispatchers.Default)` clavado en el código, el `delay`
// correría en tiempo real y el reloj virtual de `runTest` no podría adelantarlo.
class PartidoEnVivo(
    val guion: Partido,
    val msPorMinuto: Long = 10,
    private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
 
    …
 
    }.flowOn(dispatcher)

En Simulacion.kt, con los imports CoroutineDispatcher y Dispatchers, jugarPartido pasa el dispatcher:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Simulacion.kt
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
 
    …
 
suspend fun jugarPartido(
    partido: Partido,
    msPorMinuto: Long = 10,
    dispatcher: CoroutineDispatcher = Dispatchers.Default,
): Partido =
    PartidoEnVivo(partido, msPorMinuto, dispatcher).eventos()
        .fold(Partido(partido.local, partido.visitante)) { jugado, evento -> jugado.registrar(evento) }

En Cronometro.kt, con el import de CoroutineDispatcher, cambian jugarConCronometro y la cabecera de Transmisor. El parámetro dispatcher va antes de jugar (el valor por defecto de jugar lo usa, y solo puede usar parámetros anteriores) y, a diferencia de los otros dos, no lleva val: solo se usa al construir Transmisor, no se guarda como propiedad:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Cronometro.kt
import kotlinx.coroutines.CoroutineDispatcher
 
    …
 
suspend fun jugarConCronometro(
    partido: Partido,
    msPorMinuto: Long = 10,
    dispatcher: CoroutineDispatcher = Dispatchers.Default,
    alMinuto: (Int) -> Unit = {},
): Partido = coroutineScope {
    val reloj = cronometro(msPorMinuto, alMinuto)
    val final = jugarPartido(partido, msPorMinuto, dispatcher)
    reloj.cancel()
    final
}
 
    …
 
class Transmisor(
    private val alFallar: (Throwable) -> Unit = {},
    dispatcher: CoroutineDispatcher = Dispatchers.Default,
    private val jugar: suspend (Partido, Long) -> Partido = { partido, ms -> jugarConCronometro(partido, ms, dispatcher) },
) {
    private val trabajo = SupervisorJob()
    private val manejador = CoroutineExceptionHandler { _, error -> alFallar(error) }
    private val alcance = CoroutineScope(dispatcher + trabajo + manejador)
 
    …

En SegundoPlano.kt, con el mismo import, guardarEnSegundoPlano y tablaEnSegundoPlano, las dos que cambian de dispatcher (cargarTorneoEnSegundoPlano queda igual). Solo cambian la firma y el withContext.

SegundoPlano.kt: las dos funciones con el dispatcher inyectado
import kotlinx.coroutines.CoroutineDispatcher
 
suspend fun Torneo.guardarEnSegundoPlano(fichero: File, dispatcher: CoroutineDispatcher = Dispatchers.IO): Result<Unit> =
    withContext(dispatcher) { guardar(fichero) }
 
suspend fun Torneo.tablaEnSegundoPlano(dispatcher: CoroutineDispatcher = Dispatchers.Default): List<FilaDePosicion> {
    val copiaDePartidos = partidos
    val equiposInscritos = equipos
    val reglas = reglamento
    return withContext(dispatcher) { copiaDePartidos.tablaDePosiciones(equiposInscritos, reglas) }
}

Ahora arregla el test: que el partido se juegue con un dispatcher de prueba que comparta el reloj virtual del test, que compruebe también el gol del visitante (2-1), que el reloj virtual marque exactamente 90 minutos y que el partido entero tarde menos de 2 segundos de reloj real. Intenta escribirlo y, después, abre el bloque y compara. Al ejecutar ./gradlew :torneo:test --tests "*TiempoVirtualTest*" debe terminar así, en pocos segundos (el número de segundos varía):

BUILD SUCCESSFUL in 4s
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/test/java/com/sebastiannarvaez/marcador/torneo/TiempoVirtualTest.kt
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlin.system.measureTimeMillis
import kotlin.test.assertTrue
 
    …
 
    @Test
    fun unPartidoDe90MinutosSeJuegaEnMilisegundos() = runTest {
        val real = measureTimeMillis {
            val final = jugarPartido(guion, msPorMinuto = minuto, dispatcher = StandardTestDispatcher(testScheduler))
            assertEquals(2, final.golesLocal)
            assertEquals(1, final.golesVisitante)
        }
        assertEquals(90 * minuto, currentTime, "El reloj virtual debería marcar 90 minutos")
        assertTrue(real < 2_000, "Tardó $real ms de reloj real")
    }
  • measureTimeMillis { … } devuelve cuántos milisegundos de reloj real tardó el bloque.
  • assertEquals(esperado, real, "mensaje") y assertTrue(condición, "mensaje") son las de kotlin.test que usas desde Bucles y tu primer test; el último texto es el aviso que se muestra si fallan. $real mete el número en el texto.
  • real < 2_000 es un margen amplio de tiempo real: el número que no depende de tu máquina es el virtual, currentTime, que vale exactamente 5_400_000.

Ejecuta otra vez ./gradlew :torneo:test --tests "*TiempoVirtualTest*". Ahora termina en BUILD SUCCESSFUL en pocos segundos (casi todo es lo que tarda Gradle en arrancar), y el partido de 90 minutos en sí cuesta unas decenas de milisegundos. Para ver cuánto tardó cada prueba, abre el informe de Gradle desde la Terminal: open torneo/build/reports/tests/test/index.html, pulsa la clase TiempoVirtualTest y mira la columna de duración.

Con esto el partido de 90 minutos se prueba en milisegundos. En la lección siguiente, Tests con backgroundScope y Turbine, gobiernas el reloj a mano y pruebas flujos que no terminan.

Punto de control

  • En Gradle Scripts, el catálogo tiene kotlinx-coroutines-test en [libraries] y build.gradle.kts (Module :torneo) tiene la línea testImplementation(libs.kotlinx.coroutines.test). Y ./gradlew :torneo:dependencies --configuration runtimeClasspath no la nombra.
  • En la Terminal, ./gradlew :torneo:test termina en BUILD SUCCESSFUL: 128 tests en total, 0 fallos, de ellos 1 de TiempoVirtualTest. Repítelo con --rerun-tasks (obliga a Gradle a ejecutar los tests otra vez en vez de reutilizar el resultado anterior): sale igual.
  • El test de 90 minutos tarda decenas de milisegundos de reloj real (mira el informe HTML de arriba), y currentTime vale 5_400_000.
  • ./gradlew :torneo:run sigue mostrando las demos de las lecciones anteriores: los dispatchers por defecto no cambian nada en producción.
  • Los tests de tiempo real de las lecciones anteriores siguen en verde: no se han tocado.
  • Sabes decir por qué kotlinx-coroutines-test va con testImplementation y no con implementation, y por qué el dispatcher se inyecta.

Comprueba lo que entendiste

6 preguntas · necesitas 5 para completar la lección

  1. 1.¿Cómo puede runTest probar un partido de 90 minutos en milisegundos?

  2. 2.Tu test con runTest termina con UncompletedCoroutinesError: After waiting for 1m. El partido usa flowOn(Dispatchers.Default) clavado en el código. ¿Cuál es la causa y la solución?

  3. 3.Un test lanza una corrutina con StandardTestDispatcher y comprueba el resultado justo después, sin suspender. ¿Qué pasa?

  4. 4.Tras advanceTimeBy(30_000), una cuenta atrás con un delay(1_000) por número ha registrado 29 y no 30. ¿Por qué?

  5. 5.Has añadido testImplementation(libs.kotlinx.coroutines.test) en el módulo :torneo. Escribes import kotlinx.coroutines.test.runTest en Simulacion.kt, un fichero del programa, no de un test. ¿Qué ocurre al compilar?

  6. 6.En el catálogo, kotlinx-coroutines-test usa version.ref = "kotlinxCoroutinesCore" en lugar de una versión propia. ¿Por qué?

Faltan 6 de 6