Saltar al contenido
K
Corrutinas y FlowPaso a paso35 min de lectura · 5 min de máquina Intermedio

Tests con backgroundScope y Turbine

Al terminar: Probar flujos con backgroundScope, el dispatcher Unconfined e instalar Turbine.

Al terminar esta lección Marcador tendrá diez tests de tiempo virtual y sabrás recoger un flujo que no termina nunca, gobernar el reloj a mano y probar un flujo evento a evento con Turbine, la primera librería de terceros (de una empresa que no es JetBrains) que instalas, pensada solo para tests.

Es la última pieza del módulo: sin tests que corran deprisa, nadie se atreve a tocar código con corrutinas. Temas: backgroundScope, StandardTestDispatcher frente a UnconfinedTestDispatcher, y Turbine.

Los dos dispatchers de prueba

Hay dos, y los dos comparten el reloj virtual del test:

StandardTestDispatcherUnconfinedTestDispatcher
Corrutina nuevaEspera en cola hasta que el test se queda esperando (llega a un delay, un awaitItem() (de Turbine, más abajo), un join()…) o llamas a runCurrent()Arranca ya, sin esperar
OrdenExplícito: tú decides cuándo avanzaLas corrutinas corren en cuanto se lanzan
Úsalo paraCasi todoRecoger un flujo desde el principio sin runCurrent()

backgroundScope y Turbine

Un partido no termina solo si nadie cancela sus espectadores: son corrutinas infinitas. backgroundScope es un alcance del test cuyas corrutinas se cancelan al terminar el test.

Turbine es la librería para probar flujos evento a evento.

flowOf(a, b) crea un flow que emite esos valores y termina; sirve para probar sin montar un flow { }. Turbine recoge el flujo dentro del bloque test { }, que a su vez va dentro de un runTest. Este ejemplo es solo para leer (Turbine aún no está instalado en tu proyecto: lo haces en el primer paso):

ejemplo mínimo: Turbine dentro de un test (con comentarios)
import app.cash.turbine.test
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
 
class UnoDosTest {
    @Test
    fun recogeUnoYDos() = runTest {
        flowOf("uno", "dos").test {
            assertEquals("uno", awaitItem())   // espera el siguiente valor
            assertEquals("dos", awaitItem())
            awaitComplete()                    // y que el flujo termine
        }
    }
}

Gobernar el reloj de un test: un horno

Las piezas nuevas (backgroundScope, toList(lista), advanceTimeBy, runCurrent y UnconfinedTestDispatcher), en dos tests de ejemplo con un horno en vez de un partido, explicados línea a línea. Son solo para leer: no los pegues en tu proyecto (el recuento de 137 tests es el de Marcador); ya los comprobamos y pasan:

ejemplo mínimo: gobernar el reloj virtual (con comentarios)
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
 
// Un horno que avisa cada 10 minutos (600.000 ms), sin parar
fun avisosDelHorno(): Flow<Int> = flow {
    var minuto = 0
    while (true) { delay(600_000); minuto += 10; emit(minuto) }
}
 
@OptIn(ExperimentalCoroutinesApi::class)
class HornoTest {
    @Test
    fun nadaPasaHastaQueAdelantasElReloj() = runTest {
        val vistos = mutableListOf<Int>()
        // backgroundScope: se cancela solo al acabar el test (el flujo no termina nunca)
        backgroundScope.launch { avisosDelHorno().toList(vistos) }   // toList(lista): llena una lista que ya existe
        assertEquals(emptyList(), vistos)        // la corrutina aún espera en cola: no ha corrido
        advanceTimeBy(600_000 - 1)               // el reloj virtual llega a 1 ms antes del aviso
        runCurrent()                             // ejecuta lo que toca AHORA: todavía nada
        assertEquals(emptyList(), vistos)
        advanceTimeBy(1)                         // justo el minuto 10…
        runCurrent()                             // …y se ejecuta el aviso de ese instante
        assertEquals(listOf(10), vistos)
    }
 
    @Test
    fun unconfinedArrancaYa() = runTest {
        val temperatura = MutableStateFlow(20)
        val vistas = mutableListOf<Int>()
        // UnconfinedTestDispatcher: la corrutina corre YA, sin esperar a runCurrent()
        backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { temperatura.toList(vistas) }
        assertEquals(listOf(20), vistas)         // el StateFlow da su valor actual al suscribirse
        temperatura.value = 180                  // asignar a .value cambia el estado y avisa a quien lo recoge
        assertEquals(listOf(20, 180), vistas)
    }
}

Y ahora, a practicar el reloj virtual y el dispatcher que arranca al instante.

Reto · Ejercicio: el termostato en tiempo virtual

En el Kotlin Playground, sin tocar tu proyecto: el Playground trae la librería de tests de corrutinas, pero no Turbine, así que este ejercicio usa solo el reloj virtual y el dispatcher que arranca al instante.

  1. Escribe class Termostato con su temperatura como estado: MutableStateFlow(18) privado y, para el resto, un StateFlow<Int> llamado grados (con asStateFlow()). Añade suspend fun calentar(hasta: Int): mientras la temperatura sea menor que hasta, espera delay(60_000) (un minuto) y la sube un grado con _grados.update { it + 1 }.
  2. En fun main() = runTest { … } (con @OptIn(ExperimentalCoroutinesApi::class)), crea una lista mutable vistas y, dentro de backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { … }, recoge termostato.grados.toList(vistas).
  3. Imprime «Al suscribirse: » seguido de vistas; llama a termostato.calentar(21); imprime «Tras calentar: » seguido de vistas, y «Minutos virtuales: » seguido de los minutos que han pasado (currentTime / 60_000).

La consola debe mostrar:

Al suscribirse: [18]
Tras calentar: [18, 19, 20, 21]
Minutos virtuales: 3

Antes de ejecutar, predice qué imprimirá la primera línea.

Ver solución
solución: el termostato en tiempo virtual
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.*
 
class Termostato {
    private val _grados = MutableStateFlow(18)                  // el estado: empieza en 18
    val grados: StateFlow<Int> = _grados.asStateFlow()          // lo que ve el resto, solo para leer
    suspend fun calentar(hasta: Int) {
        while (_grados.value < hasta) { delay(60_000); _grados.update { it + 1 } }   // un grado por minuto
    }
}
 
@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runTest {
    val termostato = Termostato()
    val vistas = mutableListOf<Int>()
    // Unconfined: la corrutina corre YA; backgroundScope: se cancela sola al acabar
    backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { termostato.grados.toList(vistas) }
    println("Al suscribirse: $vistas")
    termostato.calentar(21)
    println("Tras calentar: $vistas")
    println("Minutos virtuales: ${currentTime / 60_000}")
}

La primera línea ya trae el 18 porque el dispatcher Unconfined arranca la corrutina al lanzarla y un StateFlow da su valor actual a quien se suscribe. Con StandardTestDispatcher la lista estaría vacía hasta el primer delay.

Paso · Instala Turbine

Objetivo: que los tests de :torneo vean Turbine, y el código normal no.

Gradle Scripts
libs.versions.toml (Version Catalog "libs")se modifica: [versions] y [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. Turbine (app.cash.turbine:turbine, de Cash App) recoge un Flow en un test y te deja pedirle los valores con awaitItem() y awaitComplete(). Como kotlinx-coroutines-test, es una librería solo para tests: va con testImplementation.

2 · [versions]. En libs.versions.toml (Version Catalog "libs"), al final de [versions]. Turbine es de otra empresa, así que lleva su propia versión (no la de las corrutinas):

catálogo: línea nueva en [versions]
turbine = "1.2.1"

3 · [libraries]. Al final de la sección:

catálogo: línea nueva en [libraries]
# Alias y versión se llaman igual y no se pisan: son secciones distintas
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }

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

5 · La dependencia. En build.gradle.kts (Module :torneo), debajo de la de kotlinx-coroutines-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)
    testImplementation(libs.kotlinx.coroutines.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(libs.turbine)                   // también testImplementation: solo de tests
}

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

7 · Comprueba. En la Terminal de Android Studio (View → Tool Windows → Terminal). Lo que ve el programa (no debe nombrar turbine):

./gradlew :torneo:dependencies --configuration runtimeClasspath

Lo que ven los tests (debe aparecer app.cash.turbine:turbine:1.2.1; filtra con | grep turbine):

./gradlew :torneo:dependencies --configuration testRuntimeClasspath

Paso · Cuatro tests con el reloj en la mano, y cinco con Turbine

Objetivo. Ampliar TiempoVirtualTest.kt hasta diez tests: dos que gobiernan el reloj con backgroundScope, dos con Turbine sobre el partido y cinco más que se pegan.

torneo → kotlin+java → com.sebastiannarvaez.marcador.torneo (test)
TiempoVirtualTest.ktse modifica
Un fichero de tests, el que creaste en la lección anterior, tal como lo ves en la vista Android.

Gobernar el reloj tú: StandardTestDispatcher y backgroundScope

Abre TiempoVirtualTest.kt: ya tiene el primer test. Un test no tiene por qué saltar todo el partido de golpe. El segundo comprueba que nada pasa hasta que se lo dices: pone a un espectador a apuntar los eventos del partido en una lista, en el alcance de fondo del test; comprueba que la lista sigue vacía hasta que adelantas el reloj, que un milisegundo antes del minuto 12 aún no hay nada, que justo en el minuto 12 aparece el gol y que, al final de los 90 minutos, ha visto los cinco eventos. Escríbelo dentro de la clase, debajo del anterior (los import que falten, con Option+Enter; usa como guía el ejemplo del horno de arriba), y compara con el bloque. Al ejecutar la clase de tests, los dos deben pasar (BUILD SUCCESSFUL).

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/test/java/com/sebastiannarvaez/marcador/torneo/TiempoVirtualTest.kt
    @Test
    fun elDispatcherEstandarSoloAvanzaCuandoSeLeDice() = runTest {
        val enVivo = PartidoEnVivo(guion, minuto, StandardTestDispatcher(testScheduler))
        val vistos = mutableListOf<EventoDePartido>()
        backgroundScope.launch { enVivo.eventos().toList(vistos) }
 
        // La corrutina se lanzó, pero un StandardTestDispatcher no la ejecuta hasta
        // que el test suspende o llama a runCurrent().
        assertEquals(0, vistos.size)
 
        advanceTimeBy(12 * minuto - 1)  // un milisegundo antes del gol del 12
        runCurrent()
        assertEquals(0, vistos.size)
 
        advanceTimeBy(1)                // en el 12: gol
        runCurrent()
        assertEquals(listOf(12), vistos.map { it.minuto })
 
        // Hasta el final. Ojo: aquí NO vale advanceUntilIdle(): lo que lanza el
        // backgroundScope es trabajo «de fondo» y advanceUntilIdle() no espera por él.
        advanceTimeBy(90 * minuto)
        runCurrent()
        assertEquals(5, vistos.size)
    }

Cómo se lee:

  • enVivo.eventos().toList(vistos) es la versión de toList que llena una lista que ya existe (vistos) en vez de devolver una nueva. Así el test puede mirar la lista mientras el flujo sigue vivo.
  • backgroundScope.launch { … } lanza el espectador en el alcance de fondo del test, que se cancela solo al terminar. Es un flujo que se recoge «para siempre»; sin backgroundScope, el test no terminaría.
  • Con un StandardTestDispatcher, la corrutina nueva espera en cola: no corre hasta que el test llega a un delay, un await… (como awaitItem()) o un join(), o llama a runCurrent(). Por eso, justo después de lanzarla, vistos sigue vacía.
  • advanceTimeBy(12 * minuto - 1) llega hasta un milisegundo antes del gol; advanceTimeBy(1) llega justo al minuto 12 y runCurrent() ejecuta lo que cae en ese instante: el gol.
  • El último comentario avisa de una trampa, que está justo debajo.

El tercero usa el otro dispatcher de prueba, el que arranca la corrutina ya: apunta el marcador de un partido desde el principio y comprueba que, nada más suscribirse y sin ejecutar nada a mano, ya ve el 0-0; después inicia el partido, deja pasar los 90 minutos y comprueba que ha visto 0-0, 1-0, 1-1 y 2-1, en ese orden.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/test/java/com/sebastiannarvaez/marcador/torneo/TiempoVirtualTest.kt
    @Test
    fun elDispatcherUnconfinedEjecutaSinEsperar() = runTest {
        val enVivo = PartidoEnVivo(guion, minuto, StandardTestDispatcher(testScheduler))
        val marcadores = mutableListOf<Marcador>()
        // Unconfined: la corrutina corre YA, sin esperar a runCurrent().
        backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { enVivo.marcador.toList(marcadores) }
        assertEquals(listOf(Marcador(0, 0)), marcadores)
        enVivo.iniciar(backgroundScope)
        advanceTimeBy(90 * minuto)
        runCurrent()
        assertEquals(listOf("0-0", "1-0", "1-1", "2-1"), marcadores.map { it.toString() })
    }

Fíjate en la diferencia con el test anterior: aquí, justo después de lanzar el espectador, marcadores ya tiene el 0-0 (el StateFlow da su valor actual a quien se suscribe). No hizo falta runCurrent(). Y enVivo.iniciar(backgroundScope) es el iniciar de StateFlow y SharedFlow, con el alcance de fondo del test.

Turbine: probar un flujo evento a evento

El cuarto test es el primero con Turbine: recoge los eventos del partido y comprueba, uno a uno y en orden, que son los del guion, que después el flujo termina y que el reloj virtual marca 90 minutos. El import que hace falta es el de la función test de Turbine, app.cash.turbine.test.

Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/test/java/com/sebastiannarvaez/marcador/torneo/TiempoVirtualTest.kt
    @Test
    fun turbineProbandoElFlowFrioEventoAEvento() = runTest {
        PartidoEnVivo(guion, minuto, StandardTestDispatcher(testScheduler)).eventos().test {
            for (esperado in guion.eventos) assertEquals(esperado, awaitItem())
            awaitComplete()
        }
        assertEquals(90 * minuto, currentTime)
    }

flujo.test { … } recoge el flujo dentro de su bloque y te da awaitItem() para pedir el siguiente valor (espera, en tiempo virtual, hasta que llega) y awaitComplete() para exigir que el flujo termine. Un for con awaitItem() comprueba los cinco eventos del guion en orden. Si el bloque termina con eventos sin consumir, Turbine falla y los lista.

El quinto lo aplica al marcador, que nunca termina: comprueba que primero llega el 0-0 actual; inicia el partido en el alcance de fondo; después deben llegar 1-0, 1-1 y 2-1, y al final nada más.

Lo nuevo de este test (expectNoEvents() y cancelAndIgnoreRemainingEvents(), para un flujo que no termina) en un ejemplo con un termo, explicado línea a línea. Como el del horno, es solo para leer: no lo pegues en tu proyecto. Ya comprobamos que pasa:

ejemplo mínimo: Turbine con un StateFlow (con comentarios)
import app.cash.turbine.test
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
 
class TermoTest {
    @Test
    fun elTermoAvisaDeCadaCambio() = runTest {
        val grados = MutableStateFlow(20)
        grados.test {                            // Turbine: recoge el flujo dentro de este bloque
            assertEquals(20, awaitItem())        // un StateFlow entrega primero su valor actual
            grados.value = 25                    // .value = cambia el estado
            assertEquals(25, awaitItem())        // espera el siguiente valor
            grados.value = 25                    // el mismo valor: un StateFlow no lo repite
            expectNoEvents()                     // comprueba que AHORA no ha llegado nada nuevo
            cancelAndIgnoreRemainingEvents()     // un StateFlow no termina: cierra sin esperar más
        }
    }
}
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
torneo/src/test/java/com/sebastiannarvaez/marcador/torneo/TiempoVirtualTest.kt
    @Test
    fun turbineProbandoElStateFlow() = runTest {
        val enVivo = PartidoEnVivo(guion, minuto, StandardTestDispatcher(testScheduler))
        enVivo.marcador.test {
            assertEquals(Marcador(0, 0), awaitItem())   // el valor actual llega primero
            enVivo.iniciar(backgroundScope)
            assertEquals(Marcador(1, 0), awaitItem())
            assertEquals(Marcador(1, 1), awaitItem())
            assertEquals(Marcador(2, 1), awaitItem())
            expectNoEvents()
            cancelAndIgnoreRemainingEvents()
        }
    }

Un StateFlow no termina nunca, así que aquí no hay awaitComplete(). expectNoEvents() comprueba que, en ese momento, no llegó nada nuevo. Y cancelAndIgnoreRemainingEvents() cierra el bloque test { } sin exigir que hayas consumido todo lo que pudiera llegar: sin esa línea, Turbine esperaría para siempre un flujo que no termina.

Las cinco pruebas que faltan

Las otras cinco son variaciones de lo que acabas de escribir (los marcadores del flujo frío, los goles del SharedFlow, la Jornada, el cronómetro y el Transmisor), así que en vez de escribirlas las pegas ya hechas. Copia el contenido del bloque de abajo y pégalo dentro de la clase, debajo de las que tienes (clic dentro de la clase, después de la última prueba; Cmd+V). Añade además los import de kotlinx.coroutines.Job, kotlinx.coroutines.cancelAndJoin y kotlinx.coroutines.delay:

TiempoVirtualTest.kt: las cinco pruebas que se pegan
    @Test
    fun turbineProbandoLosMarcadores() = runTest {
        PartidoEnVivo(guion, minuto, StandardTestDispatcher(testScheduler)).marcadores().test {
            assertEquals(Marcador(0, 0), awaitItem())
            assertEquals(Marcador(1, 0), awaitItem())
            assertEquals(12 * minuto, currentTime)
            assertEquals(Marcador(1, 1), awaitItem())
            assertEquals(Marcador(2, 1), awaitItem())
            awaitComplete()
        }
    }
 
    @Test
    fun turbineProbandoElSharedFlowDeGoles() = runTest {
        val enVivo = PartidoEnVivo(guion, minuto, StandardTestDispatcher(testScheduler))
        enVivo.goles.test {
            enVivo.iniciar(backgroundScope)
            assertEquals(listOf(12, 55, 80), List(3) { awaitItem().minuto })
            cancelAndIgnoreRemainingEvents()
        }
    }
 
    @Test
    fun laJornadaConWhileSubscribedEnTiempoVirtual() = runTest {
        val despachador = StandardTestDispatcher(testScheduler)
        val jornada = Jornada(
            PartidoEnVivo(guion, minuto, despachador),
            PartidoEnVivo(Ejemplo.lobosContraAguilas, minuto, despachador),
            backgroundScope,
        )
        jornada.marcadores.test {
            assertEquals(MarcadoresDeJornada(Marcador(0, 0), Marcador(0, 0)), awaitItem())
            jornada.iniciar()
            var ultimo = awaitItem()
            while (ultimo.golesTotales < 6) ultimo = awaitItem()
            assertEquals(MarcadoresDeJornada(Marcador(2, 1), Marcador(1, 2)), ultimo)
            cancelAndIgnoreRemainingEvents()
        }
    }
 
    @Test
    fun elRelojSeParaAlPitarElFinalEnTiempoVirtual() = runTest {
        val minutos = mutableListOf<Int>()
        val final = jugarConCronometro(guion, minuto, StandardTestDispatcher(testScheduler)) { minutos += it }
        assertEquals(2, final.golesLocal)
        val alPitar = minutos.size
        assertTrue(alPitar in 89..90, "El reloj marcó $alPitar minutos")
        delay(10 * minuto)
        assertEquals(alPitar, minutos.size, "El reloj siguió tras el final")
    }
 
    @Test
    fun elTransmisorSeCancelaAMitadDelPartidoConElRelojVirtual() = runTest {
        val terminados = mutableListOf<Partido>()
        val transmisor = Transmisor(dispatcher = StandardTestDispatcher(testScheduler))
        val trabajo: Job = transmisor.transmitir(guion, minuto) { terminados += it }
        advanceTimeBy(45 * minuto)
        runCurrent()
        transmisor.pitarElFinal()
        trabajo.cancelAndJoin()
        assertTrue(terminados.isEmpty())
        assertEquals(45 * minuto, currentTime)
    }

Cinco detalles de lo que acabas de pegar:

  • List(3) { awaitItem().minuto } crea una lista de 3 elementos llamando a la lambda tres veces: tres awaitItem() seguidos, de los que se queda con el minuto.
  • jornada.marcadores.test { … } prueba la Jornada de la lección anterior en tiempo virtual: sin espectadores no se calcula nada, y al aparecer Turbine como espectador, el combine arranca. Es la misma idea que la prueba con WhileSubscribed de la lección de StateFlow, ahora sin esperas reales.
  • while (ultimo.golesTotales < 6) ultimo = awaitItem() sigue pidiendo valores a Turbine, quedándose con el último, hasta que entre los dos partidos suman seis goles; así el test no depende de cuántos marcadores intermedios lleguen.
  • assertTrue(alPitar in 89..90): el cronómetro cuenta minutos al ritmo de msPorMinuto, y según en qué orden caiga el último tic frente al pitido final, marca 89 o 90; no es un fallo.
  • Job es el «mando» de una corrutina que viste en Alcance, Job y cancelación: cancelAndJoin() la cancela y espera a que acabe.

Ejecuta toda la suite: ./gradlew :torneo:test. Debe terminar en BUILD SUCCESSFUL.

Si tu TiempoVirtualTest.kt no compila y no encuentras por qué, puedes sustituirlo entero por uno que compila, con el botón Copiar fichero (explicado en Clases y objetos: copia el contenido al portapapeles; el fichero ya existe, solo cambias su contenido). Pierdes lo que escribiste, pero te quedas con un fichero que compila:

  1. Pulsa el botón Copiar fichero de abajo. Aparecerá un aviso de que se ha copiado.
  2. Haz clic dentro de TiempoVirtualTest.kt, selecciona todo con Cmd+A y pega con Cmd+V. El contenido pegado ya trae su línea package.
TiempoVirtualTest.kt

Punto de control

  • En Gradle Scripts, el catálogo tiene turbine = "1.2.1" en [versions] y turbine en [libraries]; build.gradle.kts (Module :torneo) tiene la línea testImplementation(libs.turbine). Y ./gradlew :torneo:dependencies --configuration runtimeClasspath no la nombra.
  • En la Terminal, ./gradlew :torneo:test termina en BUILD SUCCESSFUL: 137 tests en total, 0 fallos, de ellos 10 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 la lección anterior), 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é advanceUntilIdle() no espera al backgroundScope, y para qué sirve cancelAndIgnoreRemainingEvents().

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.Lanzas un partido en backgroundScope y llamas a advanceUntilIdle(). El marcador sigue en 0-0. ¿Por qué?

  2. 2.Pruebas con Turbine un flujo creado con flowOf("uno", "dos"). Después de los dos awaitItem(), ¿qué línea falta para que el test compruebe que el flujo terminó?

  3. 3.Pruebas con Turbine un StateFlow y, tras consumir los valores que esperabas, el bloque test { } no acaba nunca. ¿Cuál es la solución?

  4. 4.Lanzas una corrutina con UnconfinedTestDispatcher que recoge un StateFlow en una lista. Justo después, sin llamar a runCurrent(), miras la lista. ¿Qué ves?

Faltan 4 de 4