Testing de Flows con Turbine

Clase 10 de 16Curso de Android Testing

Resumen

La prueba de flujos de datos en aplicaciones Android es un aspecto crucial para garantizar la calidad del software. Turbine se presenta como una herramienta poderosa que simplifica este proceso, permitiéndonos capturar y validar cada emisión de datos de manera ordenada y efectiva. Esta capacidad resulta especialmente valiosa cuando necesitamos verificar que nuestros view models emiten estados en una secuencia específica, asegurando así el comportamiento correcto de nuestra aplicación.

¿Cómo probar flujos de datos con Turbine?

Turbine es una librería diseñada específicamente para probar flujos de datos en Kotlin, facilitando la observación y validación de cada valor emitido durante un test. Cuando trabajamos con arquitecturas basadas en estados, como MVI o MVVM, necesitamos asegurarnos de que nuestros view models emitan los estados correctos en el orden adecuado.

La función principal que nos proporciona Turbine es test(), que nos permite capturar cada emisión del flujo y realizar verificaciones sobre ella. Dentro de este contexto, podemos utilizar awaitItem() para esperar y capturar cada valor emitido, permitiéndonos hacer aserciones específicas sobre cada estado.

Implementación de pruebas con Turbine

Para implementar pruebas efectivas con Turbine, seguimos estos pasos:

  1. Configuramos nuestro test utilizando la función test() sobre el flujo que queremos probar.
  2. Utilizamos awaitItem() para capturar cada emisión del flujo.
  3. Realizamos aserciones sobre cada emisión capturada.
  4. Manejamos todas las emisiones o utilizamos funciones como cancelAndIgnoreRemainingEvents() si solo nos interesan algunas.

Veamos un ejemplo práctico:

@Test
fun givenStateWhenLoadProfileThenStateUpdate() = runTest {
    // Utilizamos test() para capturar las emisiones del flujo
    viewModel.state.test {
        // Capturamos la primera emisión (estado inicial)
        val emission1 = awaitItem()
        Truth.assertThat(emission1.isLoading).isFalse()
        
        // Ejecutamos la acción que generará nuevas emisiones
        viewModel.loadProfile()
        
        // Capturamos la segunda emisión (estado de carga)
        val emission2 = awaitItem()
        Truth.assertThat(emission2.isLoading).isTrue()
        
        // Capturamos la tercera emisión (estado final con datos)
        val emission3 = awaitItem()
        Truth.assertThat(emission3.isLoading).isFalse()
        Truth.assertThat(emission3.profile).isEqualTo(repository.profileToReturn)
    }
}

¿Por qué es importante validar cada estado en un flujo?

Cuando trabajamos con view models que emiten estados, es crucial verificar no solo el estado final, sino también los estados intermedios. Esto nos permite asegurar que la experiencia del usuario sea la correcta durante todo el proceso, mostrando indicadores de carga cuando corresponde y actualizando la interfaz en el momento adecuado.

En nuestro ejemplo, validamos tres estados importantes:

  1. Estado inicial: Verificamos que isLoading sea falso antes de realizar cualquier acción.
  2. Estado de carga: Comprobamos que isLoading cambie a verdadero cuando se inicia la carga de datos.
  3. Estado final: Confirmamos que isLoading vuelva a falso y que los datos del perfil se hayan cargado correctamente.

Manejo de emisiones no consumidas

Un aspecto importante al trabajar con Turbine es asegurarse de consumir todas las emisiones del flujo durante el test. Si no lo hacemos, Turbine lanzará un error indicando que hay eventos no consumidos (Unconsumed events found).

Para manejar esta situación, tenemos dos opciones:

  1. Consumir todas las emisiones: Utilizar awaitItem() para cada emisión esperada.
  2. Ignorar emisiones restantes: Usar cancelAndIgnoreRemainingEvents() si solo nos interesan algunas emisiones específicas.
// Si solo nos interesan las dos primeras emisiones
viewModel.state.test {
    val emission1 = awaitItem()
    // Verificaciones sobre emission1
    
    val emission2 = awaitItem()
    // Verificaciones sobre emission2
    
    // Ignoramos cualquier emisión adicional
    cancelAndIgnoreRemainingEvents()
}

¿Cómo prepararnos para pruebas con datos reales?

Las pruebas que hemos visto hasta ahora utilizan repositorios falsos que simulan datos, lo cual es útil para probar la lógica de negocio. Sin embargo, en aplicaciones reales, los datos suelen provenir de peticiones de red, lo que introduce variables adicionales como latencia, errores de conexión o respuestas inesperadas.

Para probar estos escenarios, necesitamos herramientas que simulen respuestas HTTP reales. MockWebServer es una de estas herramientas, que nos permite:

  1. Simular respuestas HTTP con diferentes códigos de estado.
  2. Introducir retrasos para probar cómo se comporta nuestra aplicación con latencia.
  3. Devolver diferentes cuerpos de respuesta para probar distintos escenarios.

Esta aproximación nos permite realizar pruebas más completas y realistas, asegurando que nuestra aplicación se comporte correctamente incluso en condiciones adversas de red.

Turbine, combinado con herramientas como MockWebServer, nos proporciona un conjunto poderoso para probar exhaustivamente nuestras aplicaciones Android, garantizando una experiencia de usuario fluida y libre de errores.

¿Has utilizado Turbine en tus proyectos? ¿Qué otras herramientas utilizas para probar flujos de datos en Android? Comparte tu experiencia en los comentarios.