Testing E2E: Test Cases
Clase 15 de 16 • Curso de Android Testing
Resumen
La implementación de pruebas end-to-end es fundamental para garantizar que todos los componentes de una aplicación funcionen correctamente en conjunto. En este artículo, exploraremos cómo crear pruebas completas para una aplicación de tareas (To-Do) utilizando Jetpack Compose, combinando UI testing, navegación, persistencia y lógica de negocio. Aprenderás a simular el flujo real de un usuario y verificar que la aplicación responde correctamente en diferentes escenarios.
¿Cómo implementar pruebas end-to-end completas en Android?
Para implementar pruebas end-to-end efectivas en Android, necesitamos combinar varios elementos: pruebas de UI, navegación, persistencia y lógica de negocio. Esto nos permite validar que todos los componentes de nuestra aplicación funcionan correctamente juntos.
En nuestro caso, utilizaremos una configuración realista de una aplicación To-Do, donde gracias a Hilt podemos inyectar dependencias reales como el TaskDAO, pero usando una base de datos en memoria para mantener nuestros tests aislados.
Configuración inicial del test
Lo primero que debemos hacer es crear una clase de prueba en nuestra carpeta de presentación:
@HiltAndroidTest
class HomeScreenTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Inject
lateinit var taskDao: TaskDao
init {
hiltRule.inject()
}
@Before
fun setup() {
runBlocking {
taskDao.deleteAllTasks()
}
}
}
En esta configuración:
- Usamos la anotación
@HiltAndroidTest
para indicar que utilizaremos inyección de dependencias con Hilt. - Configuramos
HiltAndroidRule
para manejar la inyección de dependencias. - Utilizamos
createAndroidComposeRule
con nuestraMainActivity
para poder probar la navegación y la UI. - Inyectamos nuestro
TaskDao
para poder interactuar directamente con la base de datos. - En el método
setup
, limpiamos la base de datos antes de cada prueba para asegurar un entorno controlado.
¿Por qué usamos createAndroidComposeRule
en lugar de createComposeRule
? Porque necesitamos utilizar nuestra MainActivity
real que ya tiene configurada la inyección de dependencias y la navegación que queremos probar.
Validando el estado vacío
Nuestro primer test verificará que cuando no hay tareas, se muestra el estado vacío:
@Test
fun whenNoTasks_showEmptyState() {
composeTestRule.onNodeWithContentDescription("Empty task state")
.assertIsDisplayed()
}
Este test es simple pero efectivo: verifica que cuando la base de datos está vacía (recordemos que la limpiamos en el setup
), la aplicación muestra correctamente el estado vacío.
Validando la visualización de tareas existentes
El siguiente test verifica que cuando existen tareas en la base de datos, estas se muestran correctamente en la lista:
@Test
fun whenTasksExist_showTaskList() {
runBlocking {
val testTask = TaskEntity(
id = 1,
title = "Test Task",
description = "Test description",
isCompleted = false,
category = null,
date = System.currentTimeMillis()
)
taskDao.upsertTask(testTask)
}
composeTestRule.onNodeWithContentDescription("Pending task: Test Task")
.assertIsDisplayed()
}
En este test:
- Insertamos directamente una tarea en la base de datos usando el DAO.
- Verificamos que la tarea aparece en la UI con la descripción de contenido correcta.
Este enfoque es poderoso porque nos permite validar que los datos fluyen correctamente desde la base de datos hasta la UI, pasando por todos los componentes intermedios como repositorios y ViewModels.
Probando el flujo completo de creación de tareas
Finalmente, implementamos un test que simula el flujo completo de un usuario creando una nueva tarea:
@Test
fun whenCreatingNewTask_newTaskAppearsInList() {
// Clic en el botón para agregar tarea
composeTestRule.onNodeWithContentDescription("Add new task button")
.performClick()
// Esperar a que aparezca la pantalla de creación de tarea
composeTestRule.waitUntil {
composeTestRule.onNodeWithContentDescription("Task screen is displayed")
.isDisplayed()
}
// Ingresar título de la tarea
composeTestRule.onNodeWithContentDescription("Task title input")
.performTextInput("New Test Task")
// Ingresar descripción de la tarea
composeTestRule.onNodeWithContentDescription("Task description input")
.performTextInput("New Test Description")
// Guardar la tarea
composeTestRule.onNodeWithContentDescription("Save task button")
.performClick()
// Esperar a que volvamos a la pantalla principal
composeTestRule.waitUntil {
composeTestRule.onNodeWithContentDescription("Home screen is displayed")
.isDisplayed()
}
// Verificar que la tarea aparece en la UI
composeTestRule.onNodeWithContentDescription("New Test Task")
.assertIsDisplayed()
// Verificar que la tarea se guardó en la base de datos
runBlocking {
val allTasks = taskDao.getAllTasks().first()
assert(allTasks.any { it.title == "New Test Task" })
}
}
Este test es más complejo y cubre el flujo completo:
- Navegar a la pantalla de creación de tareas.
- Ingresar los datos de la tarea.
- Guardar la tarea y volver a la pantalla principal.
- Verificar que la tarea aparece en la UI.
- Verificar que la tarea se guardó correctamente en la base de datos.
Lo más impresionante es la eficiencia: mientras que un usuario podría tardar 10-15 segundos en realizar estas acciones manualmente, el test completa todo el flujo en aproximadamente 2 segundos.
¿Por qué son importantes las pruebas end-to-end?
Las pruebas end-to-end son cruciales porque:
- Validan el flujo completo de la aplicación desde la perspectiva del usuario.
- Detectan problemas de integración entre diferentes componentes.
- Aseguran que la navegación funciona correctamente.
- Verifican que los datos persisten como se espera.
- Automatizan escenarios complejos que serían tediosos de probar manualmente.
Estas pruebas complementan otras estrategias como las pruebas unitarias y de integración, proporcionando una capa adicional de confianza en la calidad de nuestra aplicación.
Mejores prácticas para pruebas end-to-end
Al implementar pruebas end-to-end, considera estas mejores prácticas:
- Mantén los tests rápidos: Evita agregar retrasos innecesarios como
Thread.sleep()
excepto para depuración. - Usa descripciones de contenido claras: Facilita la identificación de elementos en la UI.
- Aísla el entorno de prueba: Usa bases de datos en memoria para evitar interferencias.
- Verifica tanto la UI como los datos: No solo compruebes que algo aparece en pantalla, sino también que los datos se guardan correctamente.
- Estructura tus tests lógicamente: Sigue el flujo natural del usuario para que los tests sean más fáciles de entender y mantener.
Las pruebas end-to-end son una herramienta poderosa en tu arsenal de testing, permitiéndote validar que tu aplicación funciona correctamente desde la perspectiva del usuario final. Con la combinación de Jetpack Compose, Hilt y Room, puedes crear tests robustos que te den confianza en la calidad de tu aplicación.
¿Has implementado pruebas end-to-end en tus proyectos? ¿Qué desafíos has encontrado? Comparte tu experiencia en los comentarios.