Un viaje de prueba E2E Parte 1: STUI a WebdriverIO

Publicado: 2019-11-21

Nota: Esta es una publicación de #frontend@twiliosendgrid. Para otras publicaciones de ingeniería, diríjase al blog técnico.

A medida que la arquitectura de interfaz de usuario de SendGrid comenzó a madurar en nuestras aplicaciones web, queríamos agregar otro nivel de prueba además de nuestra capa habitual de prueba de unidad e integración. Buscamos crear nuevas páginas y funciones con cobertura de prueba E2E (extremo a extremo) con herramientas de automatización del navegador.

Deseábamos automatizar las pruebas desde la perspectiva del cliente y evitar las pruebas de regresión manuales cuando fuera posible para cualquier cambio importante que pudiera ocurrir en cualquier parte de la pila. Teníamos, y todavía tenemos, el siguiente objetivo: proporcionar una forma de escribir pruebas de automatización E2E consistentes, depurables, mantenibles y valiosas para nuestras aplicaciones frontend e integrarlas con CICD (integración continua e implementación continua).

Experimentamos con múltiples enfoques técnicos hasta que finalizamos con nuestra solución ideal para las pruebas E2E. En un alto nivel, esto resume nuestro viaje:

  • Creando nuestra propia solución Ruby Selenium interna personalizada, SiteTestUI, también conocida como STUI
  • Transición de STUI a WebdriverIO basado en nodos
  • No estar satisfecho con ninguna configuración y finalmente migrar a Cypress

Esta publicación de blog es una de las dos partes que documentan y destacan nuestras experiencias, lecciones aprendidas y compensaciones con cada uno de los enfoques utilizados en el camino para guiarlo a usted y a otros desarrolladores sobre cómo conectar las pruebas E2E con patrones útiles y estrategias de prueba.

La primera parte abarca nuestras primeras luchas con STUI, cómo migramos a WebdriverIO y, sin embargo, todavía experimentamos muchas caídas similares a STUI. Repasaremos cómo escribimos pruebas con WebdriverIO, Dockerizamos las pruebas para que se ejecuten en un contenedor y, finalmente, integramos las pruebas con Buildkite, nuestro proveedor de CICD.

Si desea saltar a donde estamos con las pruebas E2E hoy, continúe con la segunda parte, ya que se trata de nuestra migración final de STUI y WebdriverIO a Cypress y cómo lo configuramos en diferentes equipos.

TLDR: Experimentamos dolores y luchas similares con las dos soluciones de envoltura de Selenium, STUI y WebdriverIO, que eventualmente comenzamos a buscar alternativas en Cypress. Aprendimos un montón de lecciones interesantes para abordar la escritura de pruebas E2E y la integración con Docker y Buildkite.

Tabla de contenido:

Primera incursión en las pruebas E2E: siteTESUI, también conocido como STUI

Cambiar de STUI a WebdriverIO

Paso 1: Decidir sobre las dependencias para WebdriverIO

Paso 2: Configuraciones y scripts del entorno

Paso 3: Implementación de pruebas ENE localmente

Paso 4: Dockerización de todas las pruebas

Paso 5: Integración con CICD

Compensaciones con WebdriverIo

Pasando a Cypress

Primera incursión en las pruebas E2E: SiteTestUI, también conocido como STUI

Cuando buscamos inicialmente una herramienta de automatización del navegador, nuestros SDET (ingenieros de desarrollo de software en prueba) se sumergieron en la creación de nuestra propia solución interna personalizada construida con Ruby y Selenium, específicamente Rspec y un marco de Selenium personalizado llamado Gridium. Valoramos su compatibilidad con todos los navegadores, la capacidad de configurar nuestras propias integraciones personalizadas con TestRail para nuestros casos de prueba de ingenieros de QA (Garantía de calidad) y la idea de crear el repositorio ideal para que todos los equipos frontend escriban pruebas E2E en una ubicación y sean ejecutar en un horario.

Como desarrollador front-end ansioso por escribir algunas pruebas E2E por primera vez con las herramientas que SDET creó para nosotros, comenzamos a implementar pruebas para las páginas que ya lanzamos y reflexionamos sobre cómo configurar correctamente los usuarios y generar datos para enfocarnos en partes de la característica que queríamos probar. Aprendimos algunas cosas geniales en el camino, como formar objetos de página para organizar la funcionalidad auxiliar y los selectores de elementos con los que deseamos interactuar por página y comenzamos a formar especificaciones que seguían esta estructura:

Gradualmente construimos conjuntos de prueba sustanciales en diferentes equipos en el mismo repositorio siguiendo patrones similares, pero pronto experimentamos muchas frustraciones que ralentizarían enormemente nuestro progreso para los nuevos desarrolladores y colaboradores constantes de STUI, como:

  • Ponerlo en funcionamiento requirió un tiempo y un esfuerzo considerables para instalar todos los controladores del navegador, las dependencias de Ruby Gem y las versiones correctas incluso antes de ejecutar las suites de prueba. A veces teníamos que averiguar por qué las pruebas se ejecutarían en la máquina de uno versus la máquina de otra persona y cómo difieren sus configuraciones.
  • Los conjuntos de pruebas proliferaron y se ejecutaron durante horas hasta su finalización. Dado que todos los equipos contribuyeron al mismo repositorio, ejecutar todas las pruebas en serie significó esperar varias horas para que se ejecutara el conjunto de pruebas general y varios equipos que impulsaron un nuevo código potencialmente llevaron a otra prueba rota en otro lugar.
  • Nos frustramos con los selectores de CSS escamosos y los complicados selectores de XPath . Esta imagen a continuación explica lo suficiente cómo el uso de XPath puede hacer las cosas más complicadas y estas fueron algunas de las más simples.

  • Las pruebas de depuración fueron dolorosas . Tuvimos problemas para depurar salidas de errores vagos y, por lo general, no teníamos idea de dónde y cómo fallaban las cosas. Solo pudimos ejecutar repetidamente las pruebas y observar el navegador para deducir dónde posiblemente falló y qué código fue el responsable. Cuando una prueba falló en un entorno Docker en CICD sin mucho que mirar más allá de los resultados de la consola, tuvimos problemas para reproducir localmente y resolver el problema.
  • Encontramos errores de Selenium y lentitud . Las pruebas se ejecutaron con lentitud debido a que todas las solicitudes se enviaban desde el servidor al navegador y, en ocasiones, nuestras pruebas fallaban por completo al intentar seleccionar muchos elementos en la página o por razones desconocidas durante las ejecuciones de prueba.
  • Se dedicó más tiempo a corregir y omitir pruebas, y las ejecuciones de pruebas de compilación programadas rotas comenzaron a ignorarse. Las pruebas no proporcionaron ningún valor al indicar errores verdaderos en el sistema.
  • Nuestros equipos de frontend se sintieron desconectados de las pruebas E2E, ya que existían en un repositorio separado de las respectivas aplicaciones web. A menudo necesitábamos tener ambos repositorios abiertos al mismo tiempo y continuar buscando entre las bases de código además de las pestañas del navegador cuando se ejecutaban las pruebas.
  • A los equipos front-end no les gustó el cambio de contexto de escribir código en JavaScript o TypeScript en el diario a Ruby y tener que volver a aprender a escribir pruebas cada vez que contribuyen a STUI.
  • Dado que fue una primera toma para muchos de nosotros cuando contribuimos a la prueba, caímos en muchos antipatrones como construir el estado a través de la interfaz de usuario para iniciar sesión, no hacer suficiente desmontaje o configuración a través de la API, y no tener suficiente documentación. a seguir por lo que hace una gran prueba.

A pesar de nuestro progreso para escribir una cantidad considerable de pruebas E2E para muchos equipos diferentes en un solo repositorio y aprender algunos patrones útiles para llevar con nosotros, experimentamos dolores de cabeza con la experiencia general del desarrollador, múltiples puntos de falla y falta de pruebas valiosas y estables. para verificar toda nuestra pila.

Valoramos una forma de empoderar a otros desarrolladores front-end y QA para crear sus propios conjuntos de pruebas E2E estables con JavaScript que reside con su propio código de aplicación para promover la reutilización, la proximidad y la propiedad de las pruebas. Esto nos llevó a probar WebdriverIO, un marco de Selenium basado en JavaScript para pruebas de automatización de navegadores, como nuestro reemplazo inicial para STUI, la solución interna personalizada de Ruby Selenium.

Más tarde experimentaríamos sus caídas y finalmente pasaríamos a Cypress (avance rápido a la Parte 2 aquí si las cosas de WebdriverIO no le atraen), pero obtuvimos una experiencia invaluable al establecer una infraestructura estandarizada en el repositorio de cada equipo, integrando pruebas E2E en CICD para nuestra interfaz. equipos y adoptando patrones técnicos que vale la pena documentar en nuestro viaje y para que otros aprendan quién puede estar a punto de saltar a WebdriverIO o cualquier otra solución de prueba E2E.

Cambiar de STUI a WebdriverIO

Cuando nos embarcamos en WebdriverIO para aliviar las frustraciones que experimentamos, experimentamos haciendo que cada equipo frontend convirtiera sus pruebas de automatización existentes escritas con el enfoque de Ruby Selenium en pruebas de WebdriverIO en JavaScript o TypeScript y comparamos la estabilidad, la velocidad, la experiencia del desarrollador y el mantenimiento general de los exámenes.

Para lograr nuestra configuración ideal de tener pruebas E2E que residen en los repositorios de aplicaciones de los equipos frontend y se ejecutan tanto en CICD como en canalizaciones programadas, recapitulamos los siguientes pasos que generalmente se aplican a cualquier equipo que desee incorporar un marco de prueba E2E con objetivos similares :

  1. Instalar y elegir dependencias para conectarse con el marco de prueba
  2. Establecimiento de configuraciones de entorno y comandos de secuencias de comandos
  3. Implementación de pruebas E2E que pasan localmente contra diferentes entornos
  4. Dockerizando las pruebas
  5. Integración de pruebas dockerizadas con el proveedor CICD

Paso 1: Decidir sobre las dependencias para WebdriverIO

WebdriverIO brinda a los desarrolladores la flexibilidad de elegir entre muchos marcos, reporteros y servicios para comenzar la prueba. Esto requirió muchos retoques e investigación para que los equipos se establecieran en ciertas bibliotecas para comenzar.

Dado que WebdriverIO no es prescriptivo sobre qué usar, abrió la puerta para que los equipos de front-end tuvieran bibliotecas y configuraciones diferentes, aunque las pruebas principales generales serían consistentes en el uso de la API de WebdriverIO.

Optamos por dejar que cada uno de los equipos de frontend se personalice según sus preferencias y, por lo general, decidimos usar Mocha como marco de prueba, Mochawesome como reportero, el servicio Selenium Standalone y soporte de Typescript. Elegimos Mocha y Mochawesome debido a la familiaridad de nuestros equipos y la experiencia previa con Mocha, pero otros equipos también decidieron usar otras alternativas.

Paso 2: Configuraciones y scripts del entorno

Después de decidirnos por la infraestructura de WebdriverIO, necesitábamos una manera de ejecutar nuestras pruebas de WebdriverIO con diferentes configuraciones para cada entorno. Aquí hay una lista que ilustra la mayoría de los casos de uso de cómo queríamos ejecutar estas pruebas y por qué deseábamos apoyarlas:

  • Contra un servidor de desarrollo de Webpack que se ejecuta en localhost (es decir, http://localhost:8000) y ese servidor de desarrollo apuntaría a una API de entorno determinada como prueba o preparación (es decir, https://testing.api.com o https:// staging.api.com).
    ¿Por qué? A veces, necesitamos realizar cambios en nuestra aplicación web local, como agregar selectores más específicos para que nuestras pruebas interactúen con los elementos de una manera más robusta o estábamos en progreso de desarrollar una nueva función y necesitábamos ajustar y validar las pruebas de automatización existentes. pasaría localmente contra nuestros nuevos cambios de código. Cada vez que el código de la aplicación cambiaba y aún no impulsábamos el entorno implementado, usamos este comando para ejecutar nuestras pruebas en nuestra aplicación web local.
  • Contra una aplicación implementada para un determinado entorno (es decir, https://testing.app.com o https://staging.app.com) como testing o staging
    ¿Por qué? Otras veces, el código de la aplicación no cambia, pero es posible que tengamos que modificar nuestro código de prueba para corregir algunas fallas o nos sentimos lo suficientemente seguros como para agregar o eliminar pruebas por completo sin realizar ningún cambio en la interfaz. Utilizamos este comando en gran medida para actualizar o depurar pruebas localmente en la aplicación implementada para simular más de cerca cómo se ejecutan nuestras pruebas en las canalizaciones de CICD.
  • Ejecución en un contenedor de Docker contra una aplicación implementada para un determinado entorno, como prueba o puesta en escena
    ¿Por qué? Esto está destinado a las canalizaciones de CICD, por lo que podemos activar las pruebas E2E para que se ejecuten en un contenedor de Docker, por ejemplo, en la aplicación implementada de prueba y asegurarnos de que pasen antes de implementar el código en producción o en ejecuciones de prueba programadas en una canalización dedicada. Al configurar estos comandos inicialmente, realizamos muchas pruebas y errores para hacer girar los contenedores Docker con diferentes valores de variables de entorno y probar para ver las pruebas adecuadas ejecutadas con éxito antes de conectarlo con nuestro proveedor de CICD, Buildkite.

Para lograr esto, configuramos un archivo de configuración base general con propiedades compartidas y muchos archivos específicos del entorno, de modo que cada archivo de configuración del entorno se fusionaría con el archivo base y sobrescribiría o agregaría propiedades según se requiera para ejecutarse. Podríamos haber tenido un archivo para cada entorno sin necesidad de un archivo base, pero eso daría lugar a una gran cantidad de duplicaciones en la configuración común. Optamos por usar una biblioteca como deepmerge para manejarlo por nosotros, pero es importante tener en cuenta que la combinación no siempre es perfecta con matrices o objetos anidados. Siempre verifique dos veces las configuraciones de salida resultantes, ya que puede conducir a un comportamiento indefinido cuando hay propiedades duplicadas que no se fusionaron correctamente.

Formamos un archivo de configuración base común , wdio.conf.js , así:

Para adaptarse a nuestro primer caso de uso importante de ejecutar pruebas E2E en un servidor de desarrollo webpack local apuntado a una API de entorno, generamos el archivo de configuración localhost, wdio.localhost.conf.js , de la siguiente manera:

Observe que fusionamos el archivo base y agregamos las propiedades específicas de localhost al archivo para hacerlo más compacto y más fácil de mantener. También usamos el servicio Selenium Standalone para activar diferentes tipos de navegadores, también conocidos como capacidades.

Para el segundo caso de uso de ejecutar pruebas E2E en una aplicación web implementada, configuramos los archivos de configuración de la aplicación de prueba y preparación , `wdio.testing.conf.js` y wdio.staging.conf.js , similar a esto:

Aquí agregamos algunas variables de entorno adicionales a los archivos de configuración, como credenciales de inicio de sesión para usuarios dedicados en la preparación y actualizamos `baseUrl` para apuntar a la URL de la aplicación de preparación implementada.

Para el tercer caso de uso de ejecutar pruebas E2E en un contenedor Docker contra una aplicación web implementada dentro del ámbito de nuestro proveedor de CICD, configuramos los archivos de configuración de CICD, wdio.cicd.testing.conf.js y wdio.cicd.staging.conf.js , así:

Observe cómo ya no usamos el servicio Selenium Standalone, ya que luego instalaremos Selenium Chrome, Selenium Hub y el código de la aplicación en servicios separados en un archivo Docker Compose. Esta configuración también muestra las mismas variables de entorno que la configuración provisional, como las credenciales de inicio de sesión y `baseUrl`, ya que esperamos ejecutar nuestras pruebas en la aplicación provisional implementada, y la única diferencia es que estas pruebas están destinadas a ejecutarse dentro de un contenedor Docker. .

Con estos archivos de configuración del entorno establecidos, describimos los comandos de script de package.json que servirían como base para nuestras pruebas. Para este ejemplo, agregamos el prefijo "uitest" a los comandos para indicar pruebas de interfaz de usuario con WebdriverIO y porque también finalizamos los archivos de prueba con *.uitest.js . Estos son algunos comandos de muestra para el entorno de ensayo:

Paso 3: Implementación de pruebas E2E localmente

Con todos los comandos de prueba disponibles, analizamos las pruebas en nuestro repositorio STUI para convertirlas a pruebas WebdriverIO. Nos enfocamos en pruebas de página de tamaño pequeño a mediano y comenzamos a aplicar el patrón de objeto de página para encapsular toda la interfaz de usuario para cada página de manera organizada.

Podríamos tener archivos estructurados con un montón de funciones de ayuda o literales de objetos o cualquier otra estrategia, pero la clave era tener una forma consistente de entregar pruebas mantenibles rápidamente y mantenerla. Si el flujo de la interfaz de usuario o los elementos DOM cambiaron para una página específica, solo necesitábamos refactorizar el objeto de la página relacionado con él y posiblemente el código de prueba para que las pruebas pasaran nuevamente.

Implementamos el patrón de objeto de página al tener un objeto de página base con funcionalidad compartida desde el cual se extenderían todos los demás objetos de página. Teníamos funciones como open para proporcionar una API coherente en todos los objetos de nuestra página para "abrir" o visitar la URL de una página en el navegador. Se parecía a algo como esto:

La implementación de los objetos de página específicos siguió el mismo patrón de extenderse desde la clase de Page base y agregar los selectores a ciertos elementos con los que deseábamos interactuar o afirmar y funciones auxiliares para realizar acciones en la página.

Observe cómo usamos la clase base abierta a través de super.open(...) con la ruta específica de la página para que podamos visitar la página con esta llamada, SomePage.open() . También exportamos la clase ya inicializada para que podamos hacer referencia a elementos como SomePage.submitButton o SomePage.tableRows e interactuar con esos elementos o afirmarlos con los comandos de WebdriverIO. Si el objeto de la página estaba destinado a compartirse e inicializarse con sus propias propiedades de miembro en un constructor, también podríamos exportar la clase directamente e instanciar el objeto de la página en los archivos de prueba con el new SomePage(...constructorArgs) .

Después de diseñar los objetos de la página con selectores y alguna funcionalidad de ayuda, luego escribimos las pruebas E2E y comúnmente modelamos esta fórmula de prueba:

  • Configure o elimine a través de la API lo que sea necesario para restablecer las condiciones de prueba al punto de inicio esperado antes de ejecutar las pruebas reales.
  • Inicie sesión en un usuario dedicado para la prueba, de modo que cada vez que visitemos páginas directamente permanezcamos conectados y no tengamos que pasar por la interfaz de usuario. Creamos una función auxiliar de inicio de login simple que toma un nombre de usuario y una contraseña que realiza la misma llamada API que usamos para nuestra página de inicio de sesión y que finalmente devuelve nuestro token de autenticación necesario para permanecer conectado y pasar los encabezados de las solicitudes API protegidas. Es posible que otras empresas tengan incluso más puntos finales internos personalizados o herramientas para crear rápidamente nuevos usuarios con datos iniciales y configuraciones, pero, lamentablemente, no teníamos uno lo suficientemente desarrollado. Lo haríamos a la antigua y crearíamos usuarios de prueba dedicados en nuestros entornos con diferentes configuraciones a través de la interfaz de usuario y, a menudo, dividiríamos las pruebas para páginas con distintos usuarios para evitar conflictos de recursos y permanecer aislados cuando las pruebas se ejecutaran en paralelo. Teníamos que asegurarnos de que los usuarios de prueba dedicados no fueran tocados por otros o, de lo contrario, las pruebas se romperían cuando alguien, sin saberlo, jugara con uno de ellos.
  • Automatice los pasos como si un usuario final interactuara con la función/página. Por lo general, visitamos la página que contiene nuestro flujo de funciones y comenzamos a seguir los pasos de alto nivel que un usuario final seguiría, como completar entradas, hacer clic en botones, esperar a que aparezcan modales o pancartas y observar tablas para salidas modificadas como un resultado de la acción. Mediante el uso de nuestros prácticos objetos de página y selectores, implementamos rápidamente cada paso y, como controles de cordura en el camino, afirmaríamos lo que el usuario debería o no debería ver en la página durante el flujo de funciones para asegurarnos de que las cosas se comportan como se esperaba. antes y después de cada paso. También decidimos deliberadamente elegir pruebas de ruta feliz de alto valor y, a veces, estados de error comunes fácilmente reproducibles, aplazando el resto de las pruebas de nivel inferior a pruebas unitarias y de integración.

Aquí hay un ejemplo aproximado del diseño general de nuestras pruebas E2E (esta estrategia se aplicó a otros marcos de prueba que también probamos):

En una nota al margen, optamos por no cubrir todos los consejos y trampas para las mejores prácticas de WebdriverIO y E2E en esta serie de publicaciones de blog, pero hablaremos sobre esos temas en una futura publicación de blog, ¡así que permanezca atento!

Paso 4: Dockerización de todas las pruebas

Al ejecutar cada paso de canalización de Buildkite en una nueva máquina de AWS en la nube, no podíamos llamar simplemente "npm run uitests:staging" porque esas máquinas no tienen Nodo, navegadores, nuestro código de aplicación o cualquier otra dependencia para ejecutar las pruebas. .

Para resolver esto, empaquetamos todas las dependencias como Node, Selenium, Chrome y el código de la aplicación en un contenedor Docker para que las pruebas de WebdriverIO se ejecuten correctamente. Aprovechamos Docker y Docker Compose para ensamblar todos los servicios necesarios para ponerlos en marcha, lo que se tradujo en Dockerfiles y docker-compose.yml y mucha experimentación con la activación local de contenedores Docker para que todo funcionara.

Para proporcionar más contexto, no éramos expertos en Docker, por lo que tomó un tiempo considerable de preparación para entender cómo poner todas las cosas juntas. Hay varias formas de Dockerize WebdriverIO pruebas y nos resultó difícil orquestar muchos servicios diferentes juntos y tamizar a través de diferentes imágenes de Docker, versiones de Compose y tutoriales hasta que todo funcionó.

Demostraremos archivos en su mayoría desarrollados que coincidieron con una de las configuraciones de nuestros equipos y esperamos que esto le brinde información a usted o a cualquier persona que aborde el problema general de las pruebas basadas en Dockerizing Selenium.

En un alto nivel, nuestras pruebas exigieron lo siguiente:

  • Selenium para ejecutar comandos y comunicarse con un navegador. Empleamos Selenium Hub para activar múltiples instancias a voluntad y descargamos la imagen, "selenium/hub", para el servicio selenium-hub en el archivo docker-compose.
  • Un navegador contra el que correr. Abrimos las instancias de Selenium Chrome e instalamos la imagen, "selenium/node-chrome-debug", para el servicio selenium-chrome en el docker-compose.yml file .
  • Código de aplicación para ejecutar nuestros archivos de prueba con cualquier otro módulo de Nodo instalado. Creamos un nuevo Dockerfile para proporcionar un entorno con Node para instalar paquetes npm y ejecutar scripts de package.json , copiar el código de prueba y asignar un servicio dedicado a ejecutar los archivos de prueba denominados uitests en el archivo docker-compose.yml .

Para abrir un servicio con toda nuestra aplicación y el código de prueba necesario para ejecutar las pruebas de WebdriverIO, creamos un Dockerfile llamado Dockerfile.uitests e instalamos todos los node_modules y copiamos el código en el directorio de trabajo de la imagen en un entorno Node. Esto sería utilizado por nuestro servicio uitests Docker Compose y logramos la configuración de Dockerfile de la siguiente manera:

Con el fin de mostrar el Selenium Hub, el navegador Chrome y el código de prueba de la aplicación para que se ejecuten las pruebas de WebdriverIO, describimos los servicios selenium-hub , selenium-chrom e y uitest s en el archivo docker-compose.uitests.yml :

Conectamos las imágenes de Selenium Hub y Chrome a través de variables de entorno, depends_on y exponiendo los puertos a los servicios. Nuestra imagen de código de aplicación de prueba eventualmente se empujaría hacia arriba y se extraería de un registro privado de Docker que administramos.

Desarrollaríamos la imagen de Docker para el código de prueba durante CICD con ciertas variables de entorno como VERSION y PIPELINE_SUFFIX para hacer referencia a las imágenes mediante una etiqueta y un nombre más específico. Luego iniciaríamos los servicios de Selenium y ejecutaríamos comandos a través del servicio uitests para ejecutar las pruebas de WebdriverIO.

A medida que creamos nuestros archivos de Docker Compose, aprovechamos los útiles comandos como docker-compose up y docker-compose down con Mac Docker instalado en nuestras máquinas para probar localmente que nuestras imágenes tenían las configuraciones adecuadas y funcionaban sin problemas antes de integrarse con Buildkite. Documentamos todos los comandos necesarios para construir las imágenes etiquetadas, subirlas al registro, bajarlas y ejecutar las pruebas de acuerdo con los valores de las variables de entorno.

Paso 5: Integración con CICD

Después de que establecimos los comandos de Docker en funcionamiento y nuestras pruebas se ejecutaron con éxito dentro de un contenedor de Docker en diferentes entornos, comenzamos a integrarnos con Buildkite, nuestro proveedor de CICD.

Buildkite proporcionó formas de ejecutar pasos en un archivo .yml en nuestras máquinas de AWS con secuencias de comandos de Bash y variables de entorno configuradas a través del código o la interfaz de usuario de configuración de Buildkite para la canalización de nuestro repositorio.

Buildkite también nos permitió activar esta canalización de prueba desde nuestra canalización de implementación principal con variables de entorno exportadas y reutilizaríamos estos pasos de prueba para otras canalizaciones de prueba aisladas que se ejecutarían según un cronograma para que nuestros controles de calidad las monitorearan y observaran.

En un nivel alto, nuestras canalizaciones de prueba de Buildkite para WebdriverIO y posteriores de Cypress compartieron los siguientes pasos similares:

  • Configure las imágenes de Docker . Cree, etiquete e inserte las imágenes de Docker requeridas para las pruebas en el registro para que podamos bajarlas en un paso posterior.
  • Ejecute las pruebas en función de las configuraciones de las variables de entorno . Extraiga las imágenes de Docker etiquetadas para la compilación específica y ejecute los comandos adecuados en un entorno implementado para ejecutar conjuntos de pruebas seleccionados a partir de las variables de entorno establecidas.

Este es un ejemplo cercano de un archivo pipeline.uitests.yml que muestra cómo configurar las imágenes de Docker en el paso "Crear imagen de Docker de UITests" y ejecutar las pruebas en el paso "Ejecutar pruebas de Webdriver en Chrome":

Una cosa a tener en cuenta es el primer paso, "Crear imagen de Docker de UITests", y cómo configura las imágenes de Docker para la prueba. Utilizó el comando de build Docker Compose para compilar el servicio uitests con todo el código de prueba de la aplicación y lo etiquetó con la variable de entorno latest y ${VERSION} para que finalmente podamos desplegar esa misma imagen con la etiqueta adecuada para esta compilación en el futuro. paso.

Cada paso puede ejecutarse en una máquina diferente en la nube de AWS en algún lugar, por lo que las etiquetas identifican de forma única la imagen para la ejecución específica de Buildkite. Después de etiquetar la imagen, subimos la última imagen etiquetada con la versión a nuestro registro privado de Docker para reutilizarla.

En el paso "Ejecutar pruebas de Webdriver contra Chrome", desplegamos la imagen que construimos, etiquetamos y empujamos en el primer paso e iniciamos Selenium Hub, Chrome y los servicios de pruebas. En función de variables de entorno como $UITESTENV y $UITESTSUITE , elegiríamos el tipo de comando para ejecutar como npm run uitest: y los conjuntos de pruebas para ejecutar esta compilación específica de Buildkite, como --suite $UITESTSUITE .

Estas variables de entorno se establecerían a través de la configuración de canalización de Buildkite o se activarían dinámicamente desde un script de Bash que analizaría un campo de selección de Buildkite para determinar qué conjuntos de pruebas ejecutar y en qué entorno.

Este es un ejemplo de pruebas de WebdriverIO desencadenadas en una canalización de pruebas dedicada, que también reutilizó el mismo archivo pipeline.uitests.yml pero con variables de entorno establecidas donde se desencadenó la canalización. Esta compilación falló y tenía capturas de pantalla de error para que las echemos un vistazo en la pestaña Artifacts y la salida de la consola en la pestaña Logs . Recuerde las artifact_paths en pipeline.uitests.yml (https://gist.github.com/alfredlucero/71032a82f3a72cb2128361c08edbcff2#file-pipeline-uitests-yml-L38), configuraciones de capturas de pantalla para `mochawesome` en `wdio.conf.js ` (https://gist.github.com/alfredlucero/4ee280be0e0674048974520b79dc993a#file-wdio-conf-js-L39), y montaje de los volúmenes en el servicio `uitests` en `docker-compose.uitests.yml` (https://gist.github.com/alfredlucero/d2df4533a4a49d5b2f2c4a0eb5590ff8#file-docker-compose-yml-L32)?

Pudimos conectar las capturas de pantalla para que fueran accesibles a través de la interfaz de usuario de Buildkite para que las descarguemos directamente y las veamos de inmediato para ayudar con las pruebas de depuración como se muestra a continuación.

Debajo se muestra otro ejemplo de pruebas de WebdriverIO que se ejecutan en una canalización separada en un cronograma para una página específica utilizando el archivo pipeline.uitests.yml , excepto con las variables de entorno ya configuradas en la configuración de canalización de Buildkite.

Es importante tener en cuenta que cada proveedor de CICD tiene una funcionalidad diferente y formas de integrar los pasos en algún tipo de proceso de implementación al fusionar código nuevo, ya sea a través de archivos .yml con sintaxis específica, configuraciones de GUI, scripts Bash o cualquier otro medio.

Cuando cambiamos de Jenkins a Buildkite, mejoramos drásticamente la capacidad de los equipos para definir sus propias canalizaciones dentro de sus respectivas bases de código, paralelizando los pasos a través de la escala de máquinas bajo demanda y utilizando comandos más fáciles de leer.

Independientemente del proveedor de CICD que pueda usar, las estrategias de integración de las pruebas serán similares en la configuración de las imágenes de Docker y la ejecución de las pruebas en función de las variables de entorno para la portabilidad y la flexibilidad.

Compensaciones con WebdriverIO

Después de convertir una cantidad considerable de las pruebas personalizadas de la solución Ruby Selenium en pruebas de WebdriverIO e integrarlas con Docker y Buildkite, mejoramos en algunas áreas, pero todavía sentimos problemas similares al sistema anterior que finalmente nos llevó a nuestra próxima y última parada con Cypress para nuestra solución de pruebas E2E.

Aquí hay una lista de algunas de las ventajas que encontramos en nuestras experiencias con WebdriverIO en comparación con la solución Ruby Selenium personalizada:

  • Las pruebas se escribieron exclusivamente en JavaScript o TypeScript en lugar de Ruby . Esto significó menos cambio de contexto entre idiomas y menos tiempo dedicado a volver a aprender Ruby cada vez que escribimos pruebas E2E.
  • Colocamos las pruebas con el código de la aplicación en lugar de hacerlo en un repositorio compartido de Ruby. Ya no nos sentimos dependientes de las fallas de las pruebas de otros equipos y asumimos una propiedad más directa de las pruebas E2E para nuestras funciones en nuestros repositorios.
  • Apreciamos la opción de prueba entre navegadores . Con WebdriverIO pudimos realizar pruebas en diferentes capacidades o navegadores como Chrome, Firefox e IE, aunque nos enfocamos principalmente en ejecutar nuestras pruebas en Chrome, ya que más del 80 % de nuestros usuarios visitaron nuestra aplicación a través de Chrome.
  • Consideramos la posibilidad de integrarse con servicios de terceros . La documentación de WebdriverIO explicó cómo integrarse con servicios de terceros como BrowserStack y SauceLabs para ayudar a cubrir nuestra aplicación en todos los dispositivos y navegadores.
  • Tuvimos la flexibilidad de elegir nuestro propio corredor de pruebas, reportero y servicios . WebdriverIO no fue prescriptivo sobre qué usar, por lo que cada equipo se tomó la libertad de decidir si usar o no cosas como Mocha y Chai o Jest y otros servicios. Esto también podría interpretarse como una estafa, ya que los equipos comenzaron a alejarse de la configuración de los demás y requirió una cantidad considerable de tiempo para experimentar con cada una de las opciones para elegir.
  • La API, la CLI y la documentación de WebdriverIO fueron lo suficientemente útiles como para escribir pruebas e integrarlas con Docker y CIC D. Podríamos tener muchos archivos de configuración diferentes, agrupar especificaciones, ejecutar pruebas a través de la línea de comandos y escribir pruebas siguiendo el patrón de objeto de página. Sin embargo, la documentación podría ser más clara y tuvimos que investigar muchos errores extraños. No obstante, pudimos convertir nuestras pruebas desde la solución Ruby Selenium.

Progresamos en muchas áreas de las que carecíamos en la solución anterior de Ruby Selenium, pero nos encontramos con muchos obstáculos que nos impidieron participar con WebdriverIO, como los siguientes:

  • Dado que WebdriverIO todavía estaba basado en Selenium , experimentamos muchos tiempos de espera, bloqueos y errores extraños, que nos recuerdan los flashbacks negativos con nuestra antigua solución Ruby Selenium. A veces, nuestras pruebas fallaban por completo cuando seleccionamos muchos elementos en la página y las pruebas se ejecutaban más lentamente de lo que nos gustaría. Tuvimos que encontrar soluciones a muchos problemas de Github o evitar ciertas metodologías al escribir pruebas.
  • La experiencia general del desarrollador fue subóptima . La documentación proporcionó una descripción general de alto nivel de los comandos, pero no suficientes ejemplos para explicar todas las formas de usarlo. Evitamos escribir pruebas E2E con Ruby y finalmente pudimos escribir pruebas en JavaScript o TypeScript, pero la API de WebdriverIO fue un poco confusa. Algunos ejemplos comunes fueron el uso de $ frente a $$ para elementos singulares frente a plurales, $('...').waitForVisible(9000, true) para esperar a que un elemento no sea visible y otros comandos poco intuitivos. Experimentamos muchos selectores escamosos y tuvimos que $(...).waitForVisible() para todo.
  • Las pruebas de depuración fueron extremadamente dolorosas y tediosas para los desarrolladores y los controles de calidad. Whenever tests failed, we only had screenshots, which would often be blank or not capturing the right moment for us to deduce what went wrong, and vague console error messages that did not point us in the right direction of how to solve the problem and even where the issue occurred. We often had to re-run the tests many times and stare closely at the Chrome browser running the tests to hopefully put things together as to where in the code our tests failed. We used things like browser.debug() but it often did not work or did not provide enough information. We gradually gathered a bunch of console error messages and mapped them to possible solutions over time but it took lots of pain and headache to get there.
  • WebdriverIO tests were tough to set up with Docker . We struggled with trying to incorporate it into Docker as there were many tutorials and ways to do things in articles online, but it was hard to figure out a way that worked in general. Hooking up 2 to 3 services together with all these configurations led to long trial and error experiments and the documentation did not guide us enough in how to do that.
  • Choosing the test runner, reporter, assertions, and services demanded lots of research time upfront . Since WebdriverIO was flexible enough to allow other options, many teams had to spend plenty of time to even have a solid WebdriverIO infrastructure after experimenting with a lot of different choices and each team can have a completely different setup that doesn't transfer over well for shared knowledge and reuse.

To summarize our WebdriverIO and STUI comparison, we analyzed the overall developer experience (related to tools, writing tests, debugging, API, documentation, etc.), test run times, test passing rates, and maintenance as displayed in this table:

Moving On to Cypress

At the end of the day, our WebdriverIO tests were still flaky and tough to maintain. More time was still spent debugging tests in dealing with weird Selenium issues, vague console errors, and somewhat useful screenshots than actually reaping the benefits of seeing tests fail for when the backend or frontend encountered issues.

We appreciated cross-browser testing and implementing tests in JavaScript, but if our tests could not pass consistently without much headache for even Chrome, then it became no longer worth it and we would then simply have a STUI 2.0.

With WebdriverIO we still strayed from the crucial aspect of providing a way to write consistent, debuggable, maintainable, and valuable E2E automation tests for our frontend applications in our original goal. Overall, we learned a lot about integrating with Buildkite and Docker, using page objects, and outlining tests in a structured way that will transfer over to our final solution with Cypress.

If we felt it was necessary to run our tests in multiple browsers and against various third-party services, we could always circle back to having some tests written with WebdriverIO, or if we needed something fully custom, we would revisit the STUI solution.

Ultimately, neither solution met our main goal for E2E tests, so follow us on our journey in how we migrated from STUI and WebdriverIO to Cypress in part 2 of the blog post series.