Testing de funciones suspendidas con runTest y runBlocking

Clase 7 de 16Curso de Android Testing

Resumen

Las aplicaciones modernas a menudo incluyen operaciones asíncronas como peticiones de red, acceso a bases de datos o llamadas a servicios remotos. En Kotlin, estas operaciones suelen realizarse mediante funciones suspendidas (suspend functions) y coroutinas. Esto implica nuevos desafíos al hacer pruebas unitarias, ya que necesitamos herramientas especiales como runBlocking y runTest de la librería Kotlin Coroutine Test para asegurar la fiabilidad y rapidez en test unitarios.

¿Qué son las funciones suspendidas en Kotlin?

Las funciones suspendidas o suspend functions permiten pausar y reanudar tareas, facilitando el trabajo en operaciones con retardo como redes o bases de datos. En nuestros ejemplos, creamos una función delayOperation:

suspend fun delayOperation(): Int {
    delay(1000L)
    return 42
}

Aquí, la palabra clave delay causa una suspensión temporal en la ejecución.

¿Cómo funciona runBlocking vs. runTest en los tests?

Para manejar estas coroutinas durante las pruebas unitarias, dos funciones comunes son:

  • runBlocking: permite llamar funciones suspendidas pero ejecuta realmente los retrasos establecidos en la operación.
  • runTest: diseñada especialmente para probar coroutinas, simula tiempo y evita retrasos reales, acelerando significativamente las pruebas.

En nuestro test con runBlocking comprobamos que, aunque pasa la prueba, tarda más de un segundo debido al delay. En cambio, usando runTest, las demoras se simulan y el test se completa casi inmediatamente, haciendo que las pruebas unitarias sean más eficientes.

¿Cómo testear flujos (flows) utilizando runTest?

Al implementar flujos (flows), que son secuencias de valores emitidos con pausas, podemos necesitar métodos específicos de prueba. Observa este ejemplo de flow:

val numberFlow = flow {
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
}

Al integrar esta estructura en pruebas, runTest permite hacer simulaciones rápidas:

  • Al usar runBlocking, cada delay es real, aumentando considerablemente el tiempo de ejecución.
  • Con runTest, esos mismos retrasos se simulan logrando ejecutar pruebas más rápidas.

Es posible incluso controlar artificialmente el paso del tiempo en un test, utilizando herramientas provistas en runTest como advanceTimeBy. De esta manera probamos que nuestras emisiones ocurran en tiempos específicos:

val numbers = mutableListOf<Int>()
val job = launch { numberFlow.collect { numbers.add(it) } }
advanceTimeBy(500)
// Hasta aquí solo debe emitirse el "1"
advanceTimeBy(600)
// Ahora debe haberse emitido también "2"

Este control brinda gran precisión y eficiencia en validaciones unitarias de código reactivo asincrónico.

Ahora que tienes más claro cómo utilizar runBlocking y runTest, ¿cuál prefieres para mejorar el desempeño en tus pruebas unitarias? ¡Comparte tu experiencia!