Fundamentos de Testing con Coroutines

Clase 7 de 16Curso de Android Testing

Resumen

La programación asíncrona es fundamental en el desarrollo de aplicaciones modernas, especialmente cuando trabajamos con operaciones que pueden tomar tiempo, como peticiones de red o consultas a bases de datos. En Kotlin, las corrutinas ofrecen una solución elegante para manejar estas operaciones, pero probarlas adecuadamente puede ser un desafío. Afortunadamente, existen herramientas específicas que nos permiten realizar pruebas unitarias efectivas y rápidas en código asíncrono.

¿Cómo realizar pruebas unitarias con corrutinas en Kotlin?

Cuando trabajamos con funciones suspendidas (suspend functions) en Kotlin, necesitamos herramientas especiales para probarlas correctamente. Estas funciones suelen implementarse en operaciones que se ejecutan en hilos separados, como llamadas a repositorios, bases de datos o servicios remotos.

La biblioteca Kotlin CoroutineTest nos proporciona utilidades como runBlocking y runTest, que nos permiten ejecutar y probar código asíncrono de manera controlada. Estas herramientas garantizan que nuestras pruebas sean fiables y predecibles, incluso cuando trabajamos con operaciones que normalmente tomarían tiempo en ejecutarse.

¿Cuál es la diferencia entre runBlocking y runTest?

Veamos un ejemplo sencillo para entender la diferencia entre estas dos herramientas:

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

@Test
fun `given delay operation when using runBlocking then waits for real delay`() = runBlocking {
    val result = delayOperation()
    assertThat(result).isEqualTo(42)
}

@Test
fun `given delay operation when using runTest then waits for delay`() = runTest {
    val result = delayOperation()
    assertThat(result).isEqualTo(42)
}

Al ejecutar estas pruebas, notaremos una diferencia significativa en el tiempo de ejecución:

  • Con runBlocking: aproximadamente 1 segundo y 52 milisegundos
  • Con runTest: solo 51 milisegundos

La diferencia es crucial: runTest omite los retrasos (delays) en el código, lo que hace que nuestras pruebas unitarias sean mucho más rápidas. Esto es especialmente importante cuando tenemos muchas pruebas que ejecutar, ya que queremos que nuestras pruebas unitarias sean lo más rápidas posible.

¿Cómo probar flujos (flows) con corrutinas?

Los flujos (flows) son una parte importante de la programación reactiva en Kotlin. Veamos cómo podemos probarlos:

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

@Test
fun `flow without time control`() = runTest {
    val numbers = mutableListOf<Int>()
    numberFlow.collect { numbers.add(it) }
    assertThat(numbers).isEqualTo(listOf(1, 2, 3))
}

Si ejecutamos esta prueba con runTest, tomará aproximadamente 93 milisegundos, mientras que con runBlocking tomaría más de 2 segundos. La diferencia de rendimiento es notable, lo que demuestra la ventaja de usar runTest para pruebas unitarias.

¿Cómo controlar el tiempo en las pruebas de corrutinas?

Una de las características más poderosas de runTest es la capacidad de controlar el tiempo durante la ejecución de la prueba. Esto nos permite verificar el estado en diferentes momentos sin tener que esperar realmente:

@Test
fun `flow with time control`() = runTest {
    val numbers = mutableListOf<Int>()
    val job = launch { numberFlow.collect { numbers.add(it) } }
    
    advanceTimeBy(500)
    assertThat(numbers).isEqualTo(listOf(1))
    
    advanceTimeBy(600)
    assertThat(numbers).isEqualTo(listOf(1, 2))
    
    advanceTimeBy(1000)
    assertThat(numbers).isEqualTo(listOf(1, 2, 3))
}

Con advanceTimeBy(), podemos avanzar el tiempo virtual en milisegundos, lo que nos permite verificar el estado de nuestra aplicación en momentos específicos. Esto es extremadamente útil para probar comportamientos que dependen del tiempo, como animaciones, temporizadores o emisiones programadas.

Si intentamos verificar un estado incorrecto, por ejemplo:

advanceTimeBy(600)
assertThat(numbers).isEqualTo(listOf(1, 2, 3)) // Esto fallará

La prueba fallará correctamente, ya que en ese momento solo deberían haberse emitido los valores 1 y 2.

Beneficios del control de tiempo en las pruebas

El control de tiempo nos permite:

  • Probar código asíncrono sin demoras reales, haciendo nuestras pruebas más rápidas
  • Verificar estados intermedios durante la ejecución de operaciones asíncronas
  • Crear pruebas más predecibles que no dependan de temporizadores del sistema real
  • Simular diferentes escenarios temporales sin cambiar el código de producción

Estas técnicas son especialmente valiosas cuando probamos componentes como ViewModels que lanzan corrutinas y actualizan su estado en respuesta a eventos asíncronos.

Las herramientas de prueba de corrutinas en Kotlin nos permiten escribir pruebas unitarias rápidas, confiables y predecibles para código asíncrono. Dominar estas técnicas es esencial para desarrollar aplicaciones robustas que manejen operaciones asíncronas correctamente. ¿Has utilizado estas herramientas en tus proyectos? Comparte tu experiencia en los comentarios.