runTest vs runBlocking em corrotinas

Resumen

Cuando una pantalla dispara una petición de red, las operaciones se ejecutan en otros hilos mediante suspend functions. Para validar ese comportamiento sin que tus tests se vuelvan lentos o impredecibles, necesitas herramientas específicas de la librería Kotlin Coroutines Test, como runBlocking y runTest, que te permiten controlar el tiempo y las emisiones de forma fiable.

¿Por qué runBlocking ralentiza tus tests unitarios?

Si intentas llamar una suspend function directamente desde un test, el compilador te lanza el error: "Suspend function should be called only from a coroutine or another suspend function". La forma más rápida de salir del paso es envolver la llamada en un bloque runBlocking.

En el ejemplo, se crea una función delayOperation dentro del paquete domain que retorna un Int después de un delay. Al ejecutarla con runBlocking, el test pasa, pero tarda 1 segundo y 53 milisegundos [02:30]. Ese tiempo es real: cada delay se respeta tal cual, y eso choca con el principio de que un test unitario debe ser lo más veloz posible.

¿Qué hace runBlocking en un test? Bloquea el hilo actual y ejecuta la corrutina respetando todos los delay reales. Sirve para casos puntuales, pero penaliza la velocidad de tu suite.

¿Cómo acelerar tests asíncronos con runTest?

La alternativa es runTest, una función pensada específicamente para escenarios de prueba con corrutinas. Cambiar runBlocking por runTest en el mismo escenario reduce el tiempo de ejecución de 1 segundo y 52 milisegundos a 51 milisegundos [04:10]. El test sigue verificando lo mismo con assertThat(result).isEqualTo(42) usando la librería Truth, pero ahora se ejecuta casi al instante.

La documentación oficial lo confirma: runTest omite los retrasos reales dentro del cuerpo de la corrutina. Esto significa que los delay se procesan de forma virtual, sin esperar tiempo de reloj.

¿Y qué pasa cuando trabajas con flows?

Los flows son emisiones propias de la programación reactiva. Imagina un flow builder que emite los números 1, 2 y 3, cada uno separado por un delay de un milisegundo. Si recolectas esas emisiones dentro de runTest añadiéndolas a una lista y comparas con listOf(1, 2, 3), el test corre en 93 milisegundos [06:40].

Cambia ese mismo test a runBlocking y el tiempo se dispara a 2 segundos y 64 milisegundos. La diferencia es brutal y deja claro cuándo conviene cada herramienta.

¿Cómo controlar el tiempo virtual en un flow con runTest?

Una de las propiedades más potentes de runTest es que te permite avanzar el tiempo de forma manual usando funciones como advanceTimeBy. Esto convierte a tu test en una especie de máquina del tiempo: tú decides cuándo deben haber ocurrido las emisiones.

El flujo de trabajo es así:

  • Lanzas un job que recolecta las emisiones del flow en segundo plano con numbersFlow.collect.
  • Avanzas el reloj virtual con advanceTimeBy(500) para forzar la primera emisión.
  • Verificas con assertThat(numbers).isEqualTo(listOf(1)) que en ese punto solo existe el primer valor.
  • Repites el patrón avanzando 600 milisegundos más para validar el 2, y luego un segundo adicional para confirmar la lista completa.

¿Qué hace advanceTimeBy en runTest? Adelanta el reloj virtual de la corrutina los milisegundos que indiques, disparando los delay pendientes sin esperar tiempo real.

Lo interesante viene cuando intentas verificar la lista completa solo 100 milisegundos después de iniciar: el test falla. La expectativa era [1, 2, 3] pero la realidad en ese instante virtual era [1, 2]. Aunque runTest ignora los retrasos reales, sigue respetando la línea temporal lógica de las emisiones.

¿Cuándo elegir runBlocking y cuándo runTest?

Usa cada uno según el contexto:

  • runBlocking: cuando necesitas ejecutar corrutinas en código de producción o en escenarios donde el tiempo real importa.
  • runTest: para tests unitarios de suspend functions y flows, donde quieres velocidad y previsibilidad.
  • advanceTimeBy: cuando quieres validar el estado intermedio de un flow en momentos específicos del tiempo virtual.

Dominar la diferencia entre tiempo real y tiempo virtual es la clave para escribir tests asíncronos rápidos sin perder rigor. El siguiente paso natural es aplicar esto a un view model que lanza corrutinas y actualiza su estado. ¿Te animas a contar en los comentarios qué herramienta usas hoy en tus tests?