El desarrollo rápido de funciones y la productividad de los ingenieros móviles se han visto frenados durante mucho tiempo por las pruebas de interfaz de usuario, un proceso lento pero esencial. Aunque las nuevas tecnologías de pruebas de interfaz de usuario automatizadas, como UI Automator o Espresso, han ayudado a los desarrolladores a escribir pruebas de interfaz de usuario, estas herramientas no mantienen el código limpio, organizado y fácil de leer. En última instancia, esto perjudica a la productividad y la escalabilidad y sigue haciendo de las pruebas de interfaz de usuario un cuello de botella en el desarrollo.
Afortunadamente, las empresas que luchan con las pruebas de interfaz de usuario pueden mejorar las herramientas de automatización de pruebas de interfaz de usuario mediante el uso de un patrón de diseño fluido para crear pruebas fáciles de leer y manejables que sean rápidas de implementar y permitan la escalabilidad.
En DoorDash, probar todos los escenarios de interfaz de usuario de una nueva versión llevaba dos días enteros a tres desarrolladores y un QA, lo que ralentizaba nuestro ciclo de desarrollo a una versión cada dos semanas. Este proceso, aunque esencial para detectar errores perjudiciales para el usuario, dañaba la moral y la productividad del equipo en general. Para resolver este problema construimos un marco basado en patrones de diseño Fluent que nos permitía utilizar herramientas de automatización de la interfaz de usuario haciendo que las pruebas fueran fáciles de leer y escalables.
Para demostrar cómo aumentamos la velocidad de nuestras pruebas, primero repasaremos los problemas de utilizar UI Automator y otros enfoques de las pruebas. A continuación, presentaremos los patrones de diseño Fluent y cómo los implementamos en DoorDash.
Los retos de las pruebas de interfaz de usuario
Las pruebas de interfaz de usuario rápidas y escalables son un reto clave para garantizar un desarrollo de aplicaciones móviles sin errores, porque las herramientas de automatización de pruebas no producen pruebas fáciles de leer y escalables, y las alternativas consumen igualmente mucho tiempo.
Aunque herramientas como UI Automator o Espresso que utilizan Android Studio han facilitado a los ingenieros empezar a escribir pruebas en Android para simular el comportamiento del usuario, por sí solas las pruebas son difíciles de entender y gestionar a escala. Si bien las pruebas pueden estar bien al principio, al aumentar el número de pruebas se hace más difícil entender el código de prueba, lo que provoca un problema de mantenimiento a largo plazo.
Las herramientas de automatización de pruebas pueden producir código de prueba en el que cada acción se describe en tres o cuatro líneas de instrucciones, en lugar de tener líneas concisas con descriptores claros, utilizando lenguaje empresarial, como se muestra en la Figura 1, a continuación:
La alternativa al uso de plataformas de pruebas automatizadas es delegar en probadores manuales. Lamentablemente, esto no ahorra realmente tiempo porque los probadores manuales requieren transferencias de conocimientos y gestionar la delegación lleva casi tanto tiempo y esfuerzo como hacer que los desarrolladores realicen las pruebas ellos mismos. Además, las pruebas manuales no son tan precisas como las automatizadas, ya que permiten más errores humanos, y no son rentables para regresiones de gran volumen.
Cómo un patrón de diseño puede ayudar a automatizar la interfaz de usuario
Los problemas que plantean las pruebas manuales de la interfaz de usuario pueden resolverse eligiendo un buen patrón de diseño y el marco adecuado para las pruebas de la interfaz de usuario. Un buen marco de pruebas de automatización debe permitir a los ingenieros escribir pruebas que sean:
- Fácil de entender y leer
- Escrito rápidamente
- Mantenible
- Escalable
Existen varios patrones de diseño que se utilizan habitualmente para las pruebas de automatización web, siendo el más popular el patrón Page Object descrito por Martin Fowler en 2013. Aplicando este patrón de diseño al ejemplo de la Figura 1, podemos ver una mejora definitiva en la legibilidad del código de prueba, como se muestra en la Figura 2, a continuación:
El código de la Figura 2 tiene mucho mejor aspecto que con el que empezamos porque:
- Cada acción puede realizarse en una línea
- Los detalles se extraen dentro de una función
- Esta función puede reutilizarse siempre que se requiera de nuevo esta acción
Sin embargo, la adopción de un patrón de diseño como éste sigue planteando algunos retos:
- La prueba sigue sin mostrar claramente su intención; en su lugar, parecen más bien instrucciones codificadas.
- Seguirá habiendo mucha duplicación de código, lo que no es lo ideal.
Uso de una interfaz fluida para resaltar la lógica empresarial
Un patrón de diseño Fluent nos proporciona lo mejor de ambos mundos, ya que demuestra una intención clara mediante el uso de un lenguaje específico del dominio. Una interfaz fluida es una API orientada a objetos cuyo diseño se basa en gran medida en el encadenamiento de métodos. El objetivo es aumentar la legibilidad del código mediante el uso de lenguaje específico del dominio, permitiendo la retransmisión del contexto de instrucciones de una llamada posterior.
Cómo un patrón de diseño Fluent demuestra una intención clara
Los patrones de diseño deben tener una intención clara y utilizar un lenguaje específico del dominio que pueda leerse casi como un lenguaje conversacional. Una interfaz fluida encaja a la perfección porque nos permite utilizar nombres de API que fluyen y un lenguaje específico del dominio.
Las ventajas de utilizar un patrón de diseño Fluent incluyen:
- Cuando el código de prueba es fácil de entender, es fácil ampliarlo y reutilizarlo.
- La facilidad de uso ayudará a los desarrolladores a trabajar con mayor rapidez y seguridad a la hora de escribir pruebas.
- El patrón de diseño es independiente de herramientas subyacentes como UI Automator o Espresso.
Utilización de un patrón de diseño Fluent para construir la automatización de pruebas de interfaz de usuario de la aplicación Dasher.
En Doordash utilizábamos TestRail para probar manualmente la aplicación antes de cada lanzamiento. Se necesitaban tres ingenieros de software y un ingeniero de control de calidad para revisar las pruebas de TestRail y medio día cada uno para ejecutarlas, lo que suponía dos días de trabajo completos. Este proceso limitaba los lanzamientos de nuestra aplicación a cada dos semanas.
El establecimiento de un nuevo marco de automatización de la interfaz de usuario para Android eliminó estos puntos conflictivos para los ciclos de lanzamiento. A continuación entraremos un poco más en detalle para explicar el enfoque y las herramientas que utilizamos, la arquitectura general de alto nivel de la solución y compartiremos algunas buenas prácticas.
Nuestro enfoque de la utilización de patrones de diseño Fluent
Por lo general, cada escenario de prueba de interfaz de usuario implica interacciones con actividades y pantallas; en cada pantalla, el usuario realizará alguna acción y esperará algún comportamiento como resultado. A continuación, utilizamos aserciones para verificar los resultados.
Para llevar a cabo estas pruebas, estructuramos el código de prueba de tal manera que cada pantalla encapsula las acciones que se realizan en esa pantalla y puede verificar el comportamiento después de realizar esas acciones. Todas las interacciones se nombran en lenguaje específico del dominio utilizando la interfaz del patrón de diseño Fluent, como se muestra en la figura 3, a continuación:
Conjunto de pruebas
La elección de las herramientas desempeña un papel importante en la mejora de la productividad de los desarrolladores. Nuestras herramientas son fáciles de usar y cuentan con un buen soporte en línea.
Herramientas de pruebas de interfaz de usuario: Antes de desarrollar nuestros procedimientos de prueba de IU, consideramos diferentes herramientas para escribir pruebas de IU, como Appium, una herramienta de terceros. Sin embargo, nos pareció que las herramientas nativas de Android eran más fáciles de usar y tenían mejor soporte. Existen dos herramientas de pruebas de interfaz de usuario compatibles con Google, que pueden ejecutarse por separado o juntas, ya que se ejecutan bajo el mismo ejecutor de pruebas de instrumentación:
- UI Automator - UI Automator es un marco de pruebas de interfaz de usuario adecuado para las pruebas funcionales de interfaz de usuario a través del sistema y las aplicaciones instaladas.
- Espresso - Una ventaja clave del uso de Espresso es que proporciona sincronización automática de las acciones de prueba con la interfaz de usuario de la aplicación que se está probando. Espresso detecta cuándo el hilo principal está inactivo, por lo que es capaz de ejecutar comandos de prueba en el momento adecuado, mejorando la fiabilidad de las pruebas.
IDE:
- Android Studio: Para los desarrolladores que creen que las pruebas de interfaz de usuario son una parte integral del desarrollo de aplicaciones, Android Studio les hará la vida más fácil. Permite ejecutar las pruebas unitarias, las pruebas de interfaz de usuario de Android y la propia aplicación desde el mismo entorno de desarrollo. También permite la estructura de paquetes de forma que el código de la aplicación y sus pruebas correspondientes (pruebas unitarias y pruebas de interfaz de usuario) puedan residir en el mismo repositorio, lo que facilita el mantenimiento de las versiones del código de la aplicación y sus pruebas correspondientes.
Dispositivos de prueba de destino:
- Las pruebas de interfaz de usuario suelen ejecutarse en un dispositivo real o en un emulador para imitar el escenario de la prueba. Para la mayoría de nuestros casos de prueba, utilizamos emuladores para configuraciones y tamaños de dispositivo habituales.
CI/CD:
- Bitrise es una de las herramientas CI/CD más populares en la nube que permite escalado y facilidad de uso para configurar entornos de prueba. Especialmente para las pruebas de interfaz de usuario, permite la integración tanto para una granja de dispositivos como para dispositivos virtuales y se ha convertido en una herramienta sencilla para configurar un entorno de compilación y pruebas para los desarrolladores.
Proceso de prueba
Escribimos escenarios de pruebas, que se muestran en la Figura 3, en un lenguaje específico del dominio siguiendo un enfoque basado en el comportamiento. Estas pruebas utilizan la API de configuración de pruebas para crear el entorno de una prueba concreta y utilizan objetos de pantalla que interactúan con las pantallas y verifican las acciones. La interacción con las pantallas y la verificación se realizan en última instancia a través de una herramienta de automatización de pruebas, como UI Automator, Espresso o cualquier otra herramienta similar.
Para entender este proceso, veamos un ejemplo del flujo de inicio de sesión de una aplicación utilizando un patrón de diseño Fluent y nuestra arquitectura de pruebas descrita anteriormente.
Prueba de inicio de sesión:
La prueba de la Figura 4 está escrita utilizando el patrón de diseño Fluent, y la clase base que permite este patrón se llama Screen.kt. El código de Screen.kt se muestra en la figura 5:
All the screen classes extend this class and follow the pattern of returning itself for each interaction/verification function, thereby passing the context along. The inline generic method “<reified T: Screen> on()” is used to switch context from one screen to another. An example of the “Screen” implementation is shown in Figure 6, below:
La implementación anterior utiliza la herramienta subyacente, UI Automator, para interactuar realmente con la pantalla. Aunque en este ejemplo se utiliza UI Automator, puede sustituirse por Espresso o cualquier otra herramienta similar sin que ello afecte a la lógica de negocio ni a las expectativas de las pruebas.
Revisión de la estructura de carpetas
Para los programadores y creadores de software limpios, la principal propiedad de un paquete es la posibilidad de tener un nombre significativo que describa su propósito y su razón de ser. Por lo tanto, hemos organizado nuestros paquetes de la siguiente manera:
- prueba: Contiene todas las pruebas de varios escenarios
- pantalla: Contiene todas las pantallas dentro de la app y las interacciones correspondientes
- UI Automator/Espresso: Contiene clases de herramientas para realizar interacciones en pantalla y verificar comportamientos.
- utils: API común para configurar el entorno para una ejecución de prueba, por ejemplo, crear y asignar órdenes antes de que comience Dashing. También contiene otras funciones de utilidad comunes.
Mejores prácticas para utilizar este enfoque
Mientras escribíamos estas pruebas, desarrollamos algunas buenas prácticas que nos ayudaron a mantener nuestro código limpio, legible y extensible. Éstas son algunas de las que seguimos:
- Convención de nombres: Incluso para la clase UIAutomator seguimos utilizando el patrón de diseño Fluent, que se lee como un lenguaje específico del dominio.
- UiAutomator.kt: Esta clase tendrá esencialmente dos tipos de funciones, cualquier acción que el usuario realice en la pantalla y la verificación del comportamiento.
- Verification function name uses this pattern: hasViewBy<Class,Text,ContenDesc,ResourceId etc that identifies the view>
- Action function name has this pattern: <click,swipe,scroll,set><Button/View>By<Button/View identifies>
- Pantalla: Es muy importante utilizar el patrón de diseño Fluent aquí, y averiguar el nombre correcto de las funciones que fluyen bien durante la lectura de la prueba.
- El nombre de la clase de la pantalla es lo que hace esa pantalla, por ejemplo PickUpItemScreen()
- El nombre de la función de verificación está en el lenguaje específico del dominio, p. ej. verifyAmount(), verifySignatureStepComplete(), verifyCompleteStepsGetCxSignature(), etc.
- El nombre de la función de acción también está en el lenguaje específico del dominio, por ejemplo, clickStartCateringSetup(), slideBeforeCateringSetupComplete(), etc.
- UiAutomator.kt: Esta clase tendrá esencialmente dos tipos de funciones, cualquier acción que el usuario realice en la pantalla y la verificación del comportamiento.
- Siempre debemos añadir un registro dentro de cada función de la clase de pantalla, lo que ayuda en la solución de problemas más rápido en CI / CD registros.
- Cualquier diálogo/hoja inferior que sea relevante para una pantalla se define como una clase anidada de la pantalla padre.
- Todas las verificaciones deberían haberse afirmado con un mensaje de registro que indique claramente el motivo del fallo de la afirmación, como se muestra en la Figura 8, a continuación:
El patrón de diseño fluido aumenta la velocidad de desarrollo
Una vez establecido el marco inicial, terminamos el 70% de nuestras pruebas de regresión en dos meses. Estos son algunos de los resultados:
- Nuestra cobertura de código pasó del 0% al ~40%.
- Nuestras pruebas manuales se hicieron cuatro veces más rápidas, pasando de 16 horas a cuatro.
- Los ciclos de lanzamiento han pasado de uno cada dos semanas a lanzamientos semanales, y podemos ejecutar pruebas de regresión siempre que queramos.
- El equipo es más productivo porque solo tenemos que escribir las pruebas para las nuevas funciones o actualizar las existentes, lo que resulta mucho más rápido de desarrollar.
Conclusión
El uso de Fluent Interface para las pruebas de interfaz de usuario liberó a los ingenieros de tareas repetitivas y laboriosas, lo que les permitió disponer de más tiempo para resolver casos extremos complicados. La mejora de la cobertura del código y la ejecución de la prueba automatizada para las pruebas de regresión garantizaron la solidez de nuestra aplicación Dasher para Android.
Dado que la estructura del código es independiente de la herramienta de pruebas subyacente (UI Automator o Espresso), podemos adoptar fácilmente cualquier herramienta mejor que se publique en el futuro.