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:
| Dispatcher | Comportamiento | Úsalo cuando |
|---|---|---|
StandardTestDispatcher | Encola las corrutinas nuevas; nada corre hasta que cedes el control | Quieres control preciso del orden |
UnconfinedTestDispatcher | Entra en las corrutinas nuevas de inmediato | Tests 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
- Reemplaza
Dispatchers.Mainen tests locales conDispatchers.setMain(testDispatcher)y restáuralo en el teardown — o envuelve el par en una regla JUnit reutilizableMainDispatcherRule. Una vez que Main es unTestDispatcher, los nuevos test dispatchers comparten su scheduler automáticamente. stateIn+WhileSubscribednecesita un colector. UnStateFlowcompartido conSharingStarted.WhileSubscribed()no producirá nada en un test hasta que algo lo coleccione — lanza primero un colector vacío enbackgroundScope.runTesttiene un timeout de 60 segundos por defecto (tiempo real, no virtual). Si se dispara, el culpable habitual es una corrutina que sigue en un dispatcher real.- Mantén
kotlinx-coroutines-testfuera del código principal — es una dependencia solo detestImplementation.
Recursos
- Documentación del módulo kotlinx-coroutines-test —
TestDispatcher,TestCoroutineScheduler, tiempo virtual - Referencia de la API
runTest - Testing de corrutinas Kotlin en Android — inyección de dispatchers,
MainDispatcherRule - Testing de flows Kotlin en Android —
backgroundScope, el detalle destateIn - Turbine — DSL de testing de Flow con
awaitItem/awaitComplete/awaitError
¿Construyendo una funcionalidad con AI? Yeda AI diseña, audita y entrega sistemas LLM de producción.