Si te estás lanzando al mundo del desarrollo en Android, probablemente ya te hayas dado cuenta de que escribir código que funcione es solo la mitad del camino; la otra mitad es asegurar que no se rompa cada vez que tocas una línea. Aquí es donde entran los tests unitarios, esa red de seguridad que nos permite dormir tranquilos sabandando que la lógica de negocio se mantiene intacta.
No se trata solo de seguir una moda, sino de adoptar una mentalidad de calidad y fiabilidad. Al principio puede parecer que pierdes tiempo escribiendo más código, pero a la larga te ahorras horas de depuración eterna y evitas que lleguen errores absurdos a manos del usuario final. Vamos a ver cómo montar todo este ecosistema usando JUnit 5 y otras herramientas imprescindibles.
Entendiendo las pruebas locales y su entorno
A diferencia de los tests de instrumentación, que requieren un dispositivo físico o un emulador, las pruebas locales corren directamente en la máquina virtual de Java (JVM) de tu ordenador. Esto hace que sean extremadamente rápidas, permitiéndote iterar la lógica de tu aplicación en cuestión de segundos.
El lugar donde deben vivir estos archivos es el directorio <code}src/test/. Es fundamental mantener una separación clara: el código productivo va en <code}src/main/ y las pruebas en su propia carpeta. Un truco muy habitual es que la clase de test tenga el mismo nombre que la original pero añadiendo el sufijo <code}Test (por ejemplo, <code}PedidoTest para la clase <code}Pedido). Al situarlas en el mismo paquete, podemos acceder a elementos que no son estrictamente públicos, facilitando la validación de detalles internos del módulo.
Configuración de dependencias en Gradle
Para que todo esto ruede, necesitamos añadir las librerías adecuadas en el archivo <code}build.gradle. Es vital usar la configuración <code}testImplementation para que estas herramientas no se incluyan en la versión final de la app que subimos a la Play Store.
- JUnit 5: La plataforma base para estructurar y ejecutar los casos de prueba.
- Mockito y Mockito-Kotlin: Fundamentales para crear simulaciones de objetos y evitar el código repetitivo gracias a la inferencia de tipos de Kotlin.
- MockK: Una alternativa muy potente y nativa de Kotlin que gestiona mejor las funciones de extensión y corrutinas.
- Robolectric: Útil cuando necesitas simular el entorno de Android (como el Context) sin necesidad de un emulador.
Dominando JUnit 5 y sus anotaciones
En JUnit 5, la unidad básica es el método marcado con la anotación <code} @Test. Este método debe ser público y no devolver ningún valor. Pero la magia ocurre cuando usamos los ciclos de vida del test para mantener el entorno limpio.
La anotación <code} @BeforeEach (o <code} @Before en versiones anteriores) es donde hacemos el setUp(). Aquí inicializamos los objetos y configuramos los mocks para que cada test empiece desde cero, evitando que el resultado de una prueba afecte a la siguiente. Por otro lado, tenemos <code} @AfterEach (o <code} @After), que sirve para el tearDown(), ideal para limpiar variables estáticas o liberar recursos que podrían contaminar el entorno.
El arte de los asertos y la verificación
Un test sin asertos es simplemente un programa que corre. Los assertions son condicionales sofisticados que, si fallan, detienen la ejecución y nos lanzan un error detallado. Los más comunes son <code}assertEquals para comparar valores, <code}assertTrue para validar booleanos y <code}assertNull para comprobar la ausencia de datos.
Cuando trabajamos con mocks, no basta con ver el resultado final; a veces necesitamos saber si una función fue llamada. Aquí entra en juego <code}verify(). Podemos comprobar si un método se ejecutó exactamente una vez, si nunca ocurrió con <code}never() o si se llamó al menos una vez con <code}atLeastOnce(). Si el orden de las llamadas es crítico, la herramienta <code}InOrder nos permite validar la secuencia exacta de interacciones entre diferentes mocks.
Simulando dependencias con Mockito
Cuando una clase depende de otras (como un Repositorio que usa un API y un DAO), no queremos probar el API real, sino el comportamiento del Repositorio. Para ello usamos objetos simulados (Mocks). Con la función <code}whenever() podemos definir qué debe responder el mock ante una llamada específica. Por ejemplo, podemos hacer que <code}getUser(any()) devuelva siempre un usuario ficticio, independientemente del ID que se le pase.
Si necesitamos algo más dinámico, como lanzar una excepción para probar la gestión de errores, podemos usar <code}doAnswer o <code}thenThrow. Esto es crucial para asegurar que nuestra app no se cierre inesperadamente cuando un servicio externo falla, sino que propaga la excepción de forma controlada o la envuelve en un error más descriptivo como un <code}IllegalArgumentException.
Estrategias en Clean Architecture
En una arquitectura limpia, el testeo se vuelve mucho más sencillo si aplicamos el principio de caja negra. No testeamos interfaces, sino implementaciones concretas. El flujo habitual es el siguiente:
- ViewModels: Se valida que la lógica de presentación reaccione correctamente a los eventos del usuario.
- Use Cases: Se comprueba que la regla de negocio se aplique sin importar de dónde vengan los datos.
- Repositories: Se verifica que el flujo de datos entre la base de datos local (DAO) y el servidor (API) sea la correcta, asegurando que los datos se cacheen adecuadamente.
Para capturar argumentos exactos que se pasan a un mock, el <code}ArgumentCaptor es la herramienta definitiva. Nos permite interceptar el objeto que se envió a una función para analizar sus propiedades detalladamente, asegurando que la información no haya sido adulterada en el camino.
Tests de Integración y casos complejos
Mientras que el test unitario aísla una clase, el test de integración verifica que varias piezas encajen. En este escenario, instanciamos clases reales para el UseCase y el Repositorio, pero mantenemos mocks en los extremos: la Vista y la Fuente de Datos. Así comprobamos que un clic en la pantalla realmente desencadene la petición y que la respuesta llegue intacta a la UI.
Para casos específicos como Retrofit, donde las funciones devuelven un objeto <code}Call, debemos mockear tanto el <code}Call como la <code}Response. Esto nos permite simular códigos de estado HTTP como el 200 OK o el 404 Not Found, obligando al código a gestionar cada escenario posible.
Tener un conjunto sólido de pruebas que abarque desde los mappers hasta los flujos de datos con Turbine, permite que el desarrollo sea fluido y escalable, transformando la incertidumbre en una certeza técnica sobre el comportamiento de cada módulo de la aplicación. Comparte la guía y otros usuarios conocerán del tema.