Yeda AI Tips · #078

English

Nunca lances corrutinas reales en los tests

¿Tests asíncronos que nunca fallan de forma intermitente? Nunca lances una corrutina real en un test. Un dispatcher real corre sobre hilos reales y tiempo real: tu test asíncrono pasa en tu máquina rápida y falla intermitentemente en un runner de CI cargado. La solución es una sola regla de diseño: inyecta el dispatcher y, en los tests, reemplázalo por uno determinista que corre sobre tiempo virtual.

Por qué los dispatchers reales fallan de forma intermitente

Cuando el código de producción escribe Dispatchers.IO o Dispatchers.Default a fuego, cada corrutina que lanza cae en un pool de hilos real. Tu test ahora compite contra el scheduler: una aserción que corre 5 ms demasiado pronto falla, un delay(500) cuesta 500 milisegundos reales, y que la corrutina lanzada haya terminado antes de tu assert depende de la carga de la máquina. Esa es la firma clásica de "pasa local, falla en CI". La guía oficial de Android es directa: no uses dispatchers reales en tests unitarios — causan problemas de hilos, tests inestables y tiempos impredecibles.

Inyecta el dispatcher

Convierte el dispatcher en un parámetro del constructor en lugar de un global fijo:

class Repository(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
    suspend fun fetchData(): String = withContext(ioDispatcher) {
        delay(500L)          // skipped instantly under a TestDispatcher
        "Hello world"
    }
}

@Test
fun fetchIsDeterministic() = runTest {
    val repository = Repository(StandardTestDispatcher(testScheduler))
    assertEquals("Hello world", repository.fetchData())
}

runTest (de kotlinx-coroutines-test) crea un TestScope respaldado por un TestDispatcher en un único hilo de test. Los delays se saltan, no se esperan: un test lleno de llamadas delay(1_000) termina en milisegundos, y el orden de ejecución sigue siendo determinista.

Controla el tiempo virtual explícitamente

Dos implementaciones de TestDispatcher, una decisión:

DispatcherComportamientoÚsalo cuando
StandardTestDispatcherEncola las corrutinas nuevas; nada corre hasta que cedes el controlQuieres control preciso del orden
UnconfinedTestDispatcherEntra en las corrutinas nuevas de inmediatoTests simples donde el orden no importa

Con el dispatcher estándar, tú manejas el reloj: advanceUntilIdle() ejecuta todo lo encolado, advanceTimeBy(duration) avanza el tiempo virtual, runCurrent() ejecuta solo lo que toca ahora mismo. La corrutina corre exactamente cuando tú lo dices, no cuando un pool de hilos tiene tiempo.

Una regla dura: un test debe tener exactamente un TestCoroutineScheduler. Cada TestDispatcher que crees debe compartirlo (StandardTestDispatcher(testScheduler)); un StandardTestDispatcher() suelto crea en silencio un segundo reloj y tu test "determinista" compite contra sí mismo.

Prueba el ciclo de vida completo de Flow

La mayoría de los tests de Flow solo verifican emisiones. Los bugs viven en los otros caminos: ¿el flow completa? ¿Propaga los errores? ¿La colección realmente se detiene con la cancelación? Para flows finitos, first() y toList() cubren las emisiones. Para todo lo demás, colecciona de forma continua con backgroundScope (se cancela automáticamente al terminar el test) o usa el DSL test {} de Turbine, que te da awaitItem(), awaitComplete() y awaitError() como aserciones de primera clase: la completación, el error y la cancelación se vuelven cosas que verificas, no cosas que esperas que salgan bien.

Notas para usuarios avanzados

Recursos

Read this article in English

¿Construyendo una funcionalidad con AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.

Habla con nosotros · Lee el blog