Descripción general de 1,000 pies sobre la escritura de pruebas Cypress #frontend@twiliosendgrid

Publicado: 2020-10-03

En Twilio SendGrid, hemos escrito cientos de pruebas de extremo a extremo (E2E) de Cypress y seguimos escribiendo más a medida que se lanzan nuevas funciones en diferentes aplicaciones web y equipos. Estas pruebas cubren toda la pila, verificando que los casos de uso más comunes por los que pasaría un cliente aún funcionan después de impulsar nuevos cambios de código en nuestras aplicaciones.

Si primero desea dar un paso atrás y leer más sobre cómo pensar sobre las pruebas E2E en general, no dude en consultar esta publicación de blog y volver a ella una vez que esté listo. Esta publicación de blog no requiere que sea un experto en pruebas E2E, pero ayuda a tener el estado de ánimo correcto, ya que verá por qué hicimos las cosas de cierta manera en nuestras pruebas. Si está buscando un tutorial más paso a paso que le presente las pruebas de Cypress, le recomendamos que consulte los documentos de Cypress . En esta publicación de blog, asumimos que puede haber visto o escrito muchas pruebas de Cypress antes y tiene curiosidad por ver cómo otros escriben pruebas de Cypress para sus propias aplicaciones.

Después de escribir muchas pruebas de Cypress, comenzará a notar que usa funciones, aserciones y patrones similares de Cypress para lograr lo que necesita. Le mostraremos las partes y estrategias más comunes que hemos usado o hecho antes con Cypress para escribir pruebas en entornos separados, como desarrollo o ensayo. Esperamos que esta descripción general de 1,000 pies de cómo escribimos las pruebas de Cypress le brinde ideas para compararlas con las suyas y lo ayude a mejorar la forma en que aborda las pruebas de Cypress.

Esquema:

  1. Resumen de la API de Cypress
  2. Interactuando con los elementos
  3. Afirmación sobre elementos
  4. Manejo de API y servicios
  5. Hacer solicitudes HTTP con cy.request(…)
  6. Crear complementos reutilizables con cy.task()
  7. Burlarse de solicitudes de red con cy.server() y cy.route()
  8. Comandos personalizados
  9. Acerca de los objetos de la página
  10. Elegir no ejecutar el código del lado del cliente con window. Cypress checks
  11. Tratando con iframes
  12. Estandarización en entornos de prueba

Resumen de la API de Cypress

Comencemos repasando las partes que hemos usado más comúnmente con la API de Cypress.

Selección de elementos

Hay muchas formas de seleccionar elementos DOM, pero puede lograr la mayor parte de lo que necesita hacer a través de estos comandos de Cypress y, por lo general, puede encadenar más acciones y aserciones después de estos.

  • Obtener elementos basados ​​en algún selector CSS con cy.get(“[data-hook='someSelector']”) o cy.find(“.selector”) .
  • Seleccionar elementos basados ​​en algún texto como cy.contains(“someText”) u obtener un elemento con un determinado selector que contiene algún texto como cy.contains(“.selector”, “someText”) .
  • Conseguir que un elemento principal mire "dentro", de modo que todas sus consultas futuras se enfocan en los elementos secundarios de los padres, como cy.get(“.selector”).within(() => { cy.get(“.child”) }) .
  • Encontrar una lista de elementos y revisar "cada uno" para realizar más consultas y afirmaciones como cy.get(“tr”).each(($tableRow) => { cy.wrap($tableRow).find('td').eq(1).should(“contain”, “someText” }) .
  • A veces, los elementos pueden estar fuera de la vista de la página, por lo que primero deberá desplazar el elemento a la vista, como cy.get(“.buttonFarBelow”).scrollIntoView() .
  • A veces necesitará un tiempo de espera más largo que el tiempo de espera del comando predeterminado, por lo que puede agregar opcionalmente { timeout: timeoutInMs } como cy.get(“.someElement”, { timeout: 10000 }) .

Interactuando con los elementos

Estas son las interacciones más utilizadas que se encuentran a lo largo de nuestras pruebas de Cypress. Ocasionalmente, deberá agregar una propiedad { force: true } en esas llamadas de función para omitir algunas comprobaciones con los elementos. Esto ocurre a menudo cuando un elemento está cubierto de alguna manera o se deriva de una biblioteca externa sobre la que no tiene mucho control en términos de cómo representa los elementos.

  • Necesitamos hacer clic en muchas cosas, como botones en modales, tablas y similares, así que hacemos cosas como cy.get(“.button”).click() .
  • Los formularios están en todas partes en nuestras aplicaciones web para completar los detalles del usuario y otros campos de datos. Escribimos en esas entradas con cy.get(“input”).type(“somekeyboardtyping”) y es posible que necesitemos borrar algunos valores predeterminados de las entradas al borrarlo primero como cy.get(“input”).clear().type(“somenewinput”) . También hay formas geniales de escribir otras teclas como {enter} para la tecla Enter cuando haces cy.get(“input”).type(“text{enter}”) .
  • Podemos interactuar con opciones de selección como cy.get(“select”).select(“value”) y casillas de verificación como cy.get(“.checkbox”).check() .

Afirmación sobre elementos

Estas son las afirmaciones típicas que puede usar en sus pruebas de Cypress para determinar si las cosas están presentes en la página con el contenido correcto.

  • Para verificar si las cosas aparecen o no en la página, puede cambiar entre cy.get(“.selector”).should(“be.visible”) y cy.get(“.selector”).should(“not.be.visible”) .
  • Para determinar si los elementos DOM existen en alguna parte del marcado y si no necesariamente le importa si los elementos son visibles, puede usar cy.get(“.element”).should(“exist”) o cy.get(“.element”).should(“not.exist”) .
  • Para ver si un elemento contiene o no algún texto, puede elegir entre cy.get(“button”).should(“contain”, “someText”) y cy.get(“button”).should(“not.contain”, “someText”) .
  • Para verificar que una entrada o un botón esté deshabilitado o habilitado, puede afirmar de esta manera: cy.get(“button”).should(“be.disabled”) .
  • Para afirmar si algo está marcado, puede probar como, cy.get(“.checkbox”).should(“be.checked”) .
  • Por lo general, puede confiar en controles de visibilidad y texto más tangibles, pero a veces debe confiar en controles de clase como cy.get(“element”).should(“have.class”, “class-name”) . También hay otras formas similares de probar atributos con .should(“have.attr”, “attribute”) .
  • A menudo, también es útil para usted encadenar afirmaciones como, cy.get(“div”).should(“be.visible”).and(“contain”, “text”) .

Manejo de API y servicios

Cuando se trata de sus propias API y servicios relacionados con el correo electrónico, puede usar cy.request(...) para realizar solicitudes HTTP a sus puntos finales de back-end con encabezados de autenticación. Otra alternativa es que puede crear cy.task(...) que se pueden llamar desde cualquier archivo de especificaciones para cubrir otras funciones que se pueden manejar mejor en un servidor Node con otras bibliotecas, como conectarse a una bandeja de entrada de correo electrónico y encontrar un hacer coincidir el correo electrónico o tener más control sobre las respuestas y el sondeo de ciertas llamadas API antes de devolver algunos valores para que los usen las pruebas.

Hacer solicitudes HTTP con cy.request(…)

Puede usar cy.request() para realizar solicitudes HTTP a su API de back-end para configurar o eliminar datos antes de que se ejecuten sus casos de prueba. Por lo general, pasa la URL del punto final, el método HTTP como "GET" o "POST", encabezados y, a veces, un cuerpo de solicitud para enviar a la API de back-end. Luego puede encadenar esto con un .then((response) => { }) para obtener acceso a la respuesta de la red a través de propiedades como "estado" y "cuerpo". Aquí se muestra un ejemplo de cómo hacer una llamada cy.request() .

A veces, es posible que no le importe si cy.request(...) fallará o no con un código de estado 4xx o 5xx durante la limpieza antes de que se ejecute una prueba. Un escenario en el que puede optar por ignorar el código de estado fallido es cuando su prueba realiza una solicitud GET para verificar si un elemento aún existe y ya se eliminó. Es posible que el elemento ya se haya limpiado y la solicitud GET fallará con un código de estado 404 no encontrado. En este caso, configuraría otra opción de failOnStatusCode: false para que sus pruebas de Cypress no fallen incluso antes de ejecutar los pasos de prueba.

Crear complementos reutilizables con cy.task()

Cuando queremos tener más flexibilidad y control sobre una función reutilizable para hablar con otro servicio, como un proveedor de bandeja de entrada de correo electrónico a través de un servidor Node (cubriremos este ejemplo en una publicación de blog posterior), nos gusta proporcionar nuestra propia funcionalidad adicional y respuestas personalizadas a las llamadas API para que las encadenemos y las apliquemos en nuestras pruebas de Cypress. O bien, nos gusta ejecutar algún otro código en un servidor Node; a menudo construimos un cy.task() para él. Creamos funciones de complemento en archivos de módulo y las importamos en plugins/index.ts donde definimos los complementos de tareas con los argumentos que necesitamos para ejecutar las funciones como se muestra a continuación.

Estos complementos se pueden llamar con cy.task(“pluginName”, { ...args }) en cualquier lugar de sus archivos de especificaciones y puede esperar que suceda la misma funcionalidad. Mientras que, si usó cy.request() , tiene menos reutilización a menos que envuelva esas llamadas en objetos de página o archivos de ayuda para importar en todas partes.

Otra advertencia es que, dado que el código de la tarea del complemento está destinado a ejecutarse en un servidor Node, no puede llamar a los comandos habituales de Cypress dentro de esas funciones, como Cypress.env(“apiHost”) o cy.getCookie('auth_token') . Pasa cosas como la cadena del token de autenticación o el host de la API de backend al objeto de argumento de la función de complemento, además de las cosas requeridas para el cuerpo de la solicitud si necesita comunicarse con su API de backend.

Burlarse de solicitudes de red con cy.server() y cy.route()

Para las pruebas de Cypress que requieren datos que son difíciles de reproducir (como variaciones de estados importantes de la interfaz de usuario en una página o tratar con llamadas API más lentas), una característica de Cypress a considerar es eliminar las solicitudes de red. Esto funciona bien con solicitudes basadas en XmlHttpRequest (XHR) si usa XMLHttpRequest estándar, la biblioteca axios o jQuery AJAX. Luego usaría cy.server() y cy.route() para escuchar rutas para simular respuestas para cualquier estado que desee. Aquí hay un ejemplo:

Otro caso de uso es usar cy.server() , cy.route() y cy.wait() juntos para escuchar y esperar a que finalicen las solicitudes de red antes de realizar los siguientes pasos. Por lo general, después de cargar una página o realizar algún tipo de acción en la página, una señal visual intuitiva indicará que algo está completo o listo para afirmar y actuar. Para los casos en los que no tiene una señal tan visible, puede esperar explícitamente a que una llamada a la API termine de esta manera.

Un problema importante es que si usa la búsqueda para las solicitudes de red, no podrá simular las solicitudes de red ni esperar a que finalicen de la misma manera. Necesitará una solución para reemplazar el window.fetch normal con un polyfill XHR y realizar algunos pasos de configuración y limpieza antes de que sus pruebas se ejecuten como se registra en estos problemas . También hay una propiedad FetchPolyfill experimentalFetchPolyfill a partir de Cypress 4.9.0 que puede funcionar para usted, pero en general, todavía estamos buscando mejores métodos para manejar el stubing de red a través del uso de fetch y XHR en nuestras aplicaciones sin que las cosas se rompan. A partir de Cypress 5.1.0, existe una nueva y prometedora función cy.route2() (consulte los documentos de Cypress ) para la creación de apéndices de red experimentales tanto para XHR como para solicitudes de obtención, por lo que planeamos actualizar nuestra versión de Cypress y experimentar con ella para ver si resuelve nuestros problemas.

Comandos personalizados

Al igual que bibliotecas como WebdriverIO, puede crear comandos personalizados globales que se pueden reutilizar y encadenar en sus archivos de especificaciones, como un comando personalizado para manejar los inicios de sesión a través de la API antes de que se ejecuten los casos de prueba. Una vez que los haya desarrollado en un archivo como support/commands.ts , puede acceder a funciones como cy.customCommand() o cy.login() . Escribir un comando personalizado para iniciar sesión se parece a esto.

Acerca de los objetos de la página

Un objeto de página es un contenedor de selectores y funciones para ayudarlo a interactuar con una página. No necesita crear objetos de página para escribir sus pruebas, pero es bueno considerar formas de encapsular los cambios en la interfaz de usuario. Quiere hacer su vida más fácil en términos de agrupar cosas para evitar actualizar los selectores y las interacciones en varios archivos en lugar de en un solo lugar.

Puede definir una clase de "Página" base con una funcionalidad común como open() para que las clases de página heredadas compartan y se extiendan. Las clases de página derivadas definen sus propias funciones getter para selectores y otras funciones auxiliares mientras reutilizan la funcionalidad de las clases base a través de llamadas como super.open() como se muestra aquí.

Elegir no ejecutar el código del lado del cliente con window. Cypress checks

Cuando probamos flujos con archivos de descarga automática, como un CSV, las descargas a menudo interrumpían nuestras pruebas de Cypress al congelar la ejecución de la prueba. Como compromiso, principalmente queríamos probar si el usuario podía alcanzar el estado de éxito adecuado para una descarga y no descargar el archivo en nuestra ejecución de prueba agregando una window.Cypress . Comprobación de Cypress.

Durante las ejecuciones de prueba de Cypress, se agregará una propiedad window.Cypress al navegador. En su código del lado del cliente, puede elegir verificar si no hay una propiedad de Cypress en el objeto de la ventana y luego realizar la descarga como de costumbre. Pero, si se ejecuta en una prueba de Cypress, no descargue el archivo. También aprovechamos la verificación de la propiedad window.Cypress para nuestros experimentos A/B que se ejecutan en nuestra aplicación web. No queríamos agregar más descamación y comportamiento no determinista de los experimentos A/B que podrían mostrar diferentes experiencias a nuestros usuarios de prueba, por lo que primero verificamos que la propiedad no esté presente antes de ejecutar la lógica del experimento como se destaca a continuación.

Tratando con iframes

Lidiar con iframes puede ser difícil con Cypress ya que no hay soporte integrado para iframes. Hay un [problema] en ejecución ( https://github.com/cypress-io/cypress/issues/136 ) lleno de soluciones para manejar iframes individuales e iframes anidados, que pueden o no funcionar dependiendo de su versión actual de Cypress o el iframe con el que desea interactuar. Para nuestro caso de uso, necesitábamos una forma de lidiar con los iframes de facturación de Zuora en nuestro entorno de ensayo para verificar los flujos de actualización de la API de correo electrónico y la API de campañas de marketing. Nuestras pruebas implican completar información de facturación de muestra antes de completar una actualización a una nueva oferta en nuestra aplicación.

Creamos un comando personalizado cy.iframe(iframeSelector) para encapsular el manejo de iframes. Al pasar un selector al iframe, se verificará el contenido del cuerpo del iframe hasta que ya no esté vacío y luego devolverá el contenido del cuerpo para que se encadene con más comandos de Cypress, como se muestra a continuación:

Cuando trabaje con TypeScript, puede escribir su comando personalizado iframe como este en su archivo index.d.ts :

Para llevar a cabo la parte de facturación de nuestras pruebas, usamos el comando personalizado iframe para obtener el contenido del cuerpo del iframe de Zuora y luego seleccionamos los elementos dentro del iframe y cambiamos sus valores directamente. Anteriormente tuvimos problemas con el uso de cy.find(...).type(...) y otras alternativas que no funcionaban, pero afortunadamente encontramos una solución al cambiar los valores de las entradas y selecciones directamente con el comando de invocación, es decir cy.get(selector).invoke('val', 'some value') . También necesitará ”chromeWebSecurity”: false en su archivo de configuración cypress.json para permitirle omitir cualquier error de origen cruzado. A continuación se proporciona un fragmento de muestra de su uso con selectores de relleno:

Estandarización en entornos de prueba

Después de escribir pruebas con Cypress usando las aserciones, funciones y enfoques más comunes resaltados anteriormente, podemos ejecutar las pruebas y hacer que pasen en un entorno. Este es un excelente primer paso, pero tenemos varios entornos para implementar código nuevo y probar nuestros cambios. Cada entorno tiene su propio conjunto de bases de datos, servidores y usuarios, pero nuestras pruebas de Cypress deben escribirse solo una vez para que funcionen con los mismos pasos generales.

Para ejecutar las pruebas de Cypress en varios entornos de prueba, como desarrollo, pruebas y preparación antes de que finalmente implementemos nuestros cambios en producción, debemos aprovechar la capacidad de Cypress para agregar variables de entorno y modificar los valores de configuración para admitir esos casos de uso.

Para ejecutar sus pruebas en diferentes entornos frontend :

Deberá cambiar el valor de "baseUrl" según se accede a través Cypress.config(“baseUrl”) para que coincida con esas URL, como https://staging.app.com o https://testing.app.com . Esto cambia la URL base para todas sus cy.visit(...) para agregar sus rutas. Hay varias formas de configurar esto, como configurar CYPRESS_BASE_URL=<frontend_url> antes de ejecutar su comando Cypress o configurar --config baseUrl=<frontend_url> .

Para ejecutar sus pruebas en diferentes entornos de back -end :

Debe conocer el nombre de host de la API, como https://staging.api.com o https://testing.api.com , para configurar una variable de entorno como "apiHost" y acceder a través de llamadas como Cypress.env(“apiHost”) . Estos se utilizarán para sus cy.request(...) para realizar solicitudes HTTP a ciertas rutas como "<apiHost>/some/endpoint" o se pasarán a sus llamadas de función cy.task(...) como otro argumento propiedad para saber qué backend golpear. Estas llamadas autenticadas también necesitarán conocer el token de autenticación que probablemente esté almacenando en localStorage o una cookie a través cy.getCookie(“auth_token”) . Asegúrese de que este token de autenticación se transfiera eventualmente como parte del encabezado "Autorización" o por algún otro medio como parte de su solicitud. Hay una multitud de formas de establecer estas variables de entorno, como directamente en el archivo cypress.json o en las opciones de línea de comandos --env donde puede hacer referencia a ellas en la documentación de Cypress .

Para abordar el inicio de sesión de diferentes usuarios o el uso de diferentes metadatos:

Ahora que sabe cómo manejar varias URL de frontend y hosts de API de backend, ¿cómo maneja el inicio de sesión de diferentes usuarios? ¿Cómo utiliza metadatos variables según el entorno, como cosas relacionadas con dominios, claves de API y otros recursos que probablemente sean únicos en los entornos de prueba?

Comencemos con la creación de otra variable de entorno llamada "testEnv" con valores posibles de "testing" y "staging" para que pueda usar esto como una forma de saber qué usuarios y metadatos del entorno aplicar en la prueba. Usando la variable de entorno "testEnv", puede abordar esto de varias maneras.

Puede crear "staging.json", "testing.json" y otros archivos JSON de entorno separados en la carpeta de fixtures e importarlos para que los use en función del valor "testEnv" como cy.fixture(`${testEnv}.json`).then(...) . Sin embargo, no puede escribir bien los archivos JSON y hay mucho más espacio para errores en la sintaxis y al escribir todas las propiedades requeridas por prueba. Los archivos JSON también están más alejados del código de prueba, por lo que deberá administrar al menos dos archivos al editar las pruebas. Se producirían problemas de mantenimiento similares si todos los datos de prueba del entorno se establecieran en variables de entorno directamente en su cypress.json y habría demasiados para administrar en una gran cantidad de pruebas.

Una opción alternativa es crear un objeto de dispositivo de prueba dentro del archivo de especificaciones con propiedades basadas en pruebas o etapas para cargar el usuario y los metadatos de esa prueba para un entorno determinado. Dado que estos son objetos, también puede definir un mejor tipo de TypeScript genérico alrededor de los objetos de dispositivo de prueba para que todos sus archivos de especificaciones se reutilicen y definan los tipos de metadatos. Llamaría a Cypress.env(“testEnv”) para ver en qué entorno de prueba se está ejecutando y usaría ese valor para extraer el dispositivo de prueba del entorno correspondiente del objeto de dispositivo de prueba general y usar esos valores en su prueba. La idea general del objeto de accesorios de prueba se resume en el fragmento de código debajo.

La aplicación conjunta del valor de configuración de Cypress "baseUrl", la variable de entorno de back-end "apiHost" y la variable de entorno "testEnv" nos permite tener pruebas de Cypress que funcionan en varios entornos sin agregar varias condiciones o flujos lógicos separados, como se demuestra a continuación.

Demos un paso atrás para ver cómo puede incluso crear sus propios comandos de Cypress para ejecutarlos a través de npm. Se pueden aplicar conceptos similares a yarn, Makefile y otros scripts que pueda estar usando para su aplicación. Es posible que desee definir variaciones de los comandos "abrir" y "ejecutar" para alinearse con Cypress "abrir" la GUI y "ejecutar" en modo sin interfaz en varios entornos de front-end y back-end en su package.json . También puede configurar varios archivos JSON para la configuración de cada entorno, pero para simplificar, verá los comandos con las opciones y los valores en línea.

Notará en los scripts de package.json que su interfaz "baseUrl" varía desde "http://localhost:9001" para cuando inicia su aplicación localmente hasta la URL de la aplicación implementada, como " https://staging.app. com ”. Puede configurar las variables "apiHost" y "testEnv" del backend para ayudar a realizar solicitudes a un punto final del backend y cargar un objeto de dispositivo de prueba específico. También puede crear comandos especiales "cicd" para cuando necesite ejecutar sus pruebas en un contenedor Docker con la clave de grabación.

algunas comidas para llevar

Cuando se trata de seleccionar elementos, interactuar con elementos y afirmar elementos en la página, puede llegar bastante lejos escribiendo muchas pruebas de Cypress con una pequeña lista de comandos de Cypress como cy.get() , cy.contains() , .click() , .type .type() , .should('be.visible') .

También hay formas de realizar solicitudes HTTP a una API de back-end usando cy.request() , ejecutar código arbitrario en un servidor Node con cy.task() y cerrar solicitudes de red usando cy.server() y cy.route() . Incluso puede crear su propio comando personalizado como cy.login() para ayudarlo a iniciar sesión en un usuario a través de la API. Todas estas cosas ayudan a restablecer a un usuario al punto de partida adecuado antes de ejecutar las pruebas. Envuelva estos selectores y funciones en un archivo y habrá creado objetos de página reutilizables para usar en sus especificaciones.

Para ayudarlo a escribir pruebas que pasen en más de un entorno, aproveche las variables de entorno y los objetos que contienen metadatos específicos del entorno.

Esto lo ayudará a ejecutar diferentes conjuntos de usuarios con recursos de datos separados en sus especificaciones de Cypress. Los comandos Cypress npm separados como npm run cypress:open:staging en su package.json cargarán los valores de variables de entorno adecuados y ejecutarán las pruebas para el entorno en el que eligió ejecutar.

Esto concluye nuestra descripción general de mil pies de escribir pruebas de Cypress. Esperamos que esto le haya proporcionado ejemplos prácticos y patrones para aplicar y mejorar en sus propias pruebas de Cypress.

¿Está interesado en obtener más información sobre las pruebas de Cypress? Consulte los siguientes recursos:

  • Qué considerar al escribir pruebas E2E
  • TypeScript Todas las cosas en sus pruebas de Cypress
  • Manejo de flujos de correo electrónico en Cypress Tests
  • Ideas para configurar, organizar y consolidar sus pruebas de Cypress
  • Integración de pruebas Cypress con Docker, Buildkite y CICD