La naturaleza asíncrona de las aplicaciones y los frameworks para dispositivos móviles a menudo dificulta escribir pruebas confiables y repetibles. Cuando se inserta un evento del usuario, el framework de pruebas debe esperar a que la app termine de reaccionar a él, lo que puede ir desde cambiar algún texto en la pantalla hasta una recreación completa de una actividad. Cuando una prueba no tiene un comportamiento determinista, es inestable.
Los frameworks modernos, como Compose o Espresso, están diseñados teniendo en cuenta las pruebas, por lo que hay una garantía de que la IU estará inactiva antes de la próxima acción o aserción de prueba. Esto es la sincronización.
Prueba la sincronización
Aún pueden surgir problemas cuando ejecutas operaciones asíncronas o en segundo plano que la prueba no conoce, como cargar datos desde una base de datos o mostrar animaciones infinitas.
Para aumentar la confiabilidad de tu paquete de pruebas, puedes instalar una forma de hacer un seguimiento de las operaciones en segundo plano, como los recursos inactivos de Espresso. Además, puedes reemplazar los módulos para probar versiones que puedes consultar para inactividad o que mejoran la sincronización, como TestDispatcher para corrutinas o RxIdler para RxJava.
Formas de mejorar la estabilidad
Las pruebas grandes pueden detectar muchas regresiones al mismo tiempo porque prueban varios componentes de una app. Por lo general, se ejecutan en emuladores o dispositivos, lo que significa que tienen alta fidelidad. Si bien las pruebas de extremo a extremo grandes proporcionan una cobertura integral, son más propensas a fallas ocasionales.
Las medidas principales que puedes tomar para reducir la fragilidad son las siguientes:
- Configurar los dispositivos correctamente
- Evita problemas de sincronización
- Implementa reintentos
Para crear pruebas grandes con Compose o Espresso, por lo general, debes iniciar una de las actividades y navegar como lo haría un usuario, para verificar que la IU se comporte correctamente con aserciones o pruebas de captura de pantalla.
Otros frameworks, como UI Automator, permiten un alcance más amplio, ya que puedes interactuar con la IU del sistema y otras apps. Sin embargo, las pruebas de UI Automator pueden requerir más sincronización manual, por lo que tienden a ser menos confiables.
Cómo configurar dispositivos
Primero, para mejorar la confiabilidad de las pruebas, debes asegurarte de que el sistema operativo del dispositivo no interrumpa de forma inesperada la ejecución de las pruebas. Por ejemplo, cuando se muestra un diálogo de actualización del sistema sobre otras apps o cuando el espacio en el disco no es suficiente.
Los proveedores de granjas de dispositivos configuran sus dispositivos y emuladores para que, por lo general, no tengas que realizar ninguna acción. Sin embargo, pueden tener sus propias directivas de configuración para casos especiales.
Dispositivos administrados por Gradle
Si administras los emuladores por tu cuenta, puedes usar los dispositivos administrados por Gradle para definir qué dispositivos usar para ejecutar tus pruebas:
android {
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
// Use device profiles you typically see in Android Studio.
device = "Pixel 2"
// Use only API levels 27 and higher.
apiLevel = 30
// To include Google services, use "google".
systemImageSource = "aosp"
}
}
}
}
}
Con esta configuración, el siguiente comando creará una imagen de emulador, iniciará una instancia, ejecutará las pruebas y la cerrará.
./gradlew pixel2api30DebugAndroidTest
Los dispositivos administrados por Gradle contienen mecanismos para volver a intentarlo en caso de desconexiones del dispositivo y otras mejoras.
Evita problemas de sincronización
Los componentes que realizan operaciones en segundo plano o asíncronas pueden provocar fallas de prueba porque se ejecutó una sentencia de prueba antes de que la IU estuviera lista para ello. A medida que el alcance de una prueba aumenta, aumentan las probabilidades de que se vuelva inestable. Estos problemas de sincronización son una fuente principal de inconsistencias, ya que los frameworks de prueba deben deducir si una actividad terminó de cargarse o si debe esperar más tiempo.
Soluciones
Puedes usar los recursos inactivos de Espresso para indicar cuándo una app está ocupada, pero es difícil hacer un seguimiento de cada operación asíncrona, en especial en pruebas de extremo a extremo muy grandes. Además, los recursos inactivos pueden ser difíciles de instalar sin contaminar el código en prueba.
En lugar de estimar si una actividad está ocupada o no, puedes hacer que las pruebas esperen hasta que se cumplan condiciones específicas. Por ejemplo, puedes esperar hasta que se muestre un texto o componente específico en la IU.
Compose tiene una colección de APIs de prueba como parte de ComposeTestRule
para esperar diferentes comparadores:
fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)
Y una API genérica que toma cualquier función que devuelve un valor booleano:
fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit
Ejemplo de uso:
composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>
Mecanismos de reintento
Debes corregir las pruebas inestables, pero a veces las condiciones que las hacen fallar son tan improbables que son difíciles de reproducir. Si bien siempre debes hacer un seguimiento de las pruebas inestables y corregirlas, un mecanismo de reintento puede ayudar a mantener la productividad del desarrollador, ya que ejecuta la prueba varias veces hasta que se aprueba.
Los reintentos deben realizarse en varios niveles para evitar problemas, como los siguientes:
- Se agotó el tiempo de espera de la conexión al dispositivo o se perdió la conexión
- Falla en una sola prueba
La instalación o configuración de reintentos depende de tus frameworks de pruebas y de la infraestructura, pero los mecanismos típicos incluyen los siguientes:
- Una regla de JUnit que vuelve a intentar cualquier prueba varias veces
- Una acción o un paso de reintento en tu flujo de trabajo de CI
- Un sistema para reiniciar un emulador cuando no responde, como los dispositivos administrados por Gradle.