Cómo usar pruebas dobles en Android

Cuando se diseña la estrategia de prueba para un elemento o sistema, hay tres aspectos de prueba relacionados:

  • Alcance: ¿Qué porcentaje del código toca la prueba? Las pruebas pueden verificar un solo método, toda la aplicación o algún punto intermedio. El alcance probado está en prueba y suele referirse a él como Sujeto que se está probando, aunque también como Sistema a prueba o Unidad bajo prueba.
  • Velocidad: ¿Qué tan rápido se ejecuta la prueba? Las velocidades de prueba pueden variar de milisegundos a varios minutos.
  • Fidelidad: ¿Qué tan “real” es la prueba? Por ejemplo, si parte del código que estás probando necesita realizar una solicitud de red, ¿el código de prueba realmente realiza esta solicitud de red o falsifica el resultado? Si la prueba realmente se comunica con la red, significa que tiene mayor fidelidad. La desventaja es que la prueba podría tardar más tiempo en ejecutarse, generar errores si la red está inactiva o podría ser costosa.

Consulta qué debes probar para obtener información sobre cómo comenzar a definir tu estrategia de prueba.

Aislamiento y dependencias

Cuando pruebas un elemento o un sistema de elementos, lo haces en aislamiento. Por ejemplo, para probar un ViewModel, no necesitas iniciar un emulador y, luego, iniciar una IU, ya que no depende (o no debería) depender del framework de Android.

Sin embargo, el sujeto de prueba puede depender de otros para que funcione. Por ejemplo, un ViewModel podría depender de un repositorio de datos para funcionar.

Cuando necesitas proporcionar una dependencia a un sujeto de prueba, una práctica común es crear un doble de prueba (o un objeto de prueba). Los dobles de prueba son objetos que se ven y actúan como componentes en tu app, pero se crean en tu prueba para proporcionar un comportamiento o datos específicos. La ventaja principal es que permiten que las pruebas sean más rápidas y sencillas.

Tipos de dobles de prueba

Existen varios tipos de dobles de prueba:

Falso Es un doble de prueba que tiene una implementación "funciona" de la clase, pero que se implementa de una manera que es adecuada para pruebas, pero no apta para producción.

Ejemplo: una base de datos en la memoria

Las simulaciones no requieren un framework de simulación y son ligeras. Son preferidos.

Simulación Es un doble de prueba que se comporta de la manera en que lo programas para que se comporte y que tiene expectativas sobre sus interacciones. Las simulaciones no aprobarán las pruebas si sus interacciones no coinciden con los requisitos que definas. Por lo general, las simulaciones se crean con un framework de simulación para lograr todo esto.

Ejemplo: Verifica que se haya llamado a un método de una base de datos exactamente una vez.

Stub Es un doble de prueba que comporta la manera en que lo programas para que se comporte, pero que no tiene expectativas sobre sus interacciones. Por lo general, se crea con un framework de simulación. Se prefieren las falsificaciones en lugar de los stubs para simplificar.
Cuenta ficticia Un doble de prueba que se pasa, pero que no se usa, por ejemplo, si solo necesitas proporcionarlo como parámetro.

Ejemplo: Una función vacía pasada como una devolución de llamada de clic

Espía Wrapper sobre un objeto real que también realiza un seguimiento de información adicional, similar a las simulaciones. Por lo general, se evitan para agregar complejidad. Por lo tanto, se prefieren las falsificaciones o las burlas en lugar de los espías.
Shadow Es la falsificación que se usó en Robolectric.

Ejemplo con una identidad falsa

Supongamos que quieres realizar una prueba de unidades de un ViewModel que depende de una interfaz llamada UserRepository y expone el nombre del primer usuario a una IU. Puedes crear un doble de prueba falsa implementando la interfaz y mostrando datos conocidos.

object FakeUserRepository : UserRepository {
    fun getUsers() = listOf(UserAlice, UserBob)
}

val const UserAlice = User("Alice")
val const UserBob = User("Bob")

Este UserRepository falso no necesita depender de las fuentes de datos locales y remotas que usaría la versión de producción. El archivo se aloja en el conjunto de orígenes de prueba y no se enviará con la app de producción.

Una dependencia falsa puede mostrar datos conocidos sin depender de fuentes de datos remotas.
Figura 1: Una dependencia falsa en una prueba de unidades.

En la siguiente prueba, se verifica que ViewModel expone correctamente el primer nombre de usuario a la vista.

@Test
fun viewModelA_loadsUsers_showsFirstUser() {
    // Given a VM using fake data
    val viewModel = ViewModelA(FakeUserRepository) // Kicks off data load on init

    // Verify that the exposed data is correct
    assertEquals(viewModel.firstUserName, UserAlice.name)
}

Es fácil reemplazar UserRepository por uno falso en una prueba de unidades, ya que el verificador crea el ViewModel. Sin embargo, puede ser difícil reemplazar elementos arbitrarios en pruebas más grandes.

Reemplazo de componentes e inserción de dependencias

Cuando las pruebas no controlan la creación de los sistemas a prueba, el reemplazo de componentes por dobles de prueba se vuelve más complicado y requiere que la arquitectura de tu app siga un diseño que se pueda probar.

Incluso las pruebas grandes de extremo a extremo pueden beneficiarse del uso de dobles de prueba, como una prueba de IU instrumentada que navega por un flujo de usuarios completo en tu app. En ese caso, te recomendamos que la prueba sea hermética. Una prueba hermética evita todas las dependencias externas, como la recuperación de datos de Internet. Esto mejora la confiabilidad y el rendimiento.

Figura 2: Una prueba importante que abarca la mayor parte de la app y falsifica datos remotos.

Puedes diseñar tu app para lograr esta flexibilidad de forma manual, pero te recomendamos que uses un framework de inserción de dependencias, como Hilt, para reemplazar los componentes de la app en el momento de la prueba. Consulta la guía de prueba de Hilt.

Robolectric

En Android, puedes usar el framework de Robolectric, que proporciona un tipo especial de doble de prueba. Robolectric te permite ejecutar las pruebas en tu estación de trabajo o en tu entorno de integración continua. Usa una JVM normal, sin un emulador o dispositivo. Simula el aumento de vistas, la carga de recursos y otras partes del framework de Android con dobles de prueba llamados sombras.

Robolectric es un simulador, por lo que no debe reemplazar pruebas de unidades simples ni usarse para realizar pruebas de compatibilidad. En algunos casos, proporciona velocidad y reduce los costos a expensas de una menor fidelidad. Un buen enfoque para las pruebas de IU es hacer que sean compatibles con las pruebas instrumentadas y de Robolectric, y decidir cuándo ejecutarlas según sea necesario probar la funcionalidad o la compatibilidad. Las pruebas de Espresso y Compose pueden ejecutarse en Robolectric.