Crear tests end to end con JavaScript y Cypress

Crear tests end to end con JavaScript y Cypress te facilita la vida como desarrollador web. Aprende a implementar las bases en una app real, siguiendo el tutorial que he preparado.

Crear test end to end con JavaScript y Cypress
Crear test end to end con JavaScript y Cypress

Desarrollar aplicaciones es apasionante, en eso estamos todos de acuerdo. 

Es simplemente satisfactorio empezar un proyecto nuevo, y ver como va cogiendo forma poco a poco. 

No obstante, a medida que un programa va creciendo en funcionalidades, también se vuelve más complejo de mantener.

Además, al adquirir una cierta dimensión, es habitual ampliar el equipo de desarrollo, y por consiguiente, aparecen más ramas que manejar en el repositorio.

Inevitablemente eso conduce a la introducción de errores no intencionados, o lo que comúnmente llamamos «bugs». 

Por eso, se vuelve imprescindible realizar comprobaciones periódicas de rendimiento y funcionalidad.

Este tipo de tareas se denominan tests

Estas comprobaciones suelen realizarse interactuando con la interfaz de usuario, en busca de bugs, o comportamientos erráticos, que eventualmente hayan podido aparecer. 

Pero como imaginarás, interactuar con la aplicación web de forma repetitiva buscando errores, es tedioso e implica dedicarle mucho tiempo.

Por no mencionar que hacerlo a mano es poco eficaz, ya que fácilmente pueden quedarse tests sin realizar.

Por esto, voy a dedicar este post a hablarte de Cypress, la librería JavaScript para la automatización de tests.

Tests “end to end” y tests unitarios.

Antes de entrar en materia, debes saber que existen distintos tipos de tests según lo que se desee validar

Por un lado, están los tests unitarios, pequeñas pruebas que comprueban el comportamiento y la lógica de una parte del código, como una función, un método o una clase.

Estas pruebas se realizan aisladas del resto del programa, de este modo se garantiza su correcto funcionamiento, independientemente del contexto.

Es por eso que resultan especialmente útiles para detectar y corregir posibles errores, antes de integrar el código en otras partes del sistema.

Por otro lado, existen los test end to end, también conocidos como tests «e2e».

El testing e2e es una forma de probar una aplicación de software simulando el uso real que le daría un usuario.

El objetivo es verificar que todas las partes de la aplicación funcionan correctamente y cumplen con los requisitos esperados.

Un ejemplo sería reproducir el proceso de compra que realiza un usuario a través de una tienda online.

Desde buscar el producto y añadirlo en el carrito de compra, hasta pagar y recibir un correo de confirmación.

A pesar de que Cypress permite realizar tanto tests unitarios como end to end, en ésta publicación me centraré en explicar únicamente el segundo tipo.

La cantidad de beneficios que aporta la implementación de esta tecnología, en seguida salta a la vista. 

  • Escribir un test una sola vez y que éste se realice incansablemente tantas veces como sea necesario. 
  • Asentar una metodología de trabajo, que define buenas prácticas. 
  • Ayuda a prevenir la publicación de bugs en producción. 
  • Mejorar la robustez de la UI.
  • Detectar rápidamente si un cambio provoca errores en un código antiguo.

Herramientas como Cypress o Playwright, se están convirtiendo en módulos esenciales para la comunidad de desarrolladores JavaScript.

Datos como los que te presento a continuación dan fe de ello.

El repositorio de Github de Cypress tiene más de 45.2k estrellas.

Se encuentra activamente mantenido por un equipo de más de 460 personas. Y su paquete NPM se descarga una media de 5.2 millones de veces a la semana.

Emular la interacción con el navegador

Como hemos visto, a grandes rasgos, el objetivo de Cypress a la hora de realizar tests e2e, es el de simular la interacción de un usuario con la interfaz web a través del navegador.

En cierto modo puedes entender Cypress como una suerte de robot que tiene como único propósito probar una y otra vez una aplicación.

A lo largo de este post iremos viendo cómo implementarlo en un proyecto real.

Paso a paso irás conociendo su metodología y te familiarizaras con los conceptos básicos del testing e2e.

Cypress se adapta a todo tipo de proyectos web, así que siéntete libre de ponerlo en práctica sobre un proyecto que hayas creado.

En cualquier caso, para poder seguir esta guía he dejado preparada una aplicación web, desarrollada con MongoDb, Express, React y Node.

Encontrarás el código fuente en el siguiente enlace.

https://github.com/Danivalldo/libreriasjs/tree/master/cypress

Lee atentamente el archivo Readme para configurar y levantar el proyecto en tu ordenador. Ten en cuenta que es necesario disponer de conexión a una base de datos MongoDb, puedes usar Atlas como servicio gratuito.

En este mismo repositorio también encontrarás los tests completos que iremos generando, puedes usarlos de referencia, si algún punto de la guía no se entiende bien.

Tras clonar el repositorio y lanzarlo en local, deberías ver un sitio web parecido a éste.

Ver proyecto para testear con Cypress. Haz click en la imagen para abrirlo en una ventana nueva.
Ver proyecto para testear con Cypress. Haz click en la imagen para abrirlo en una ventana nueva.

Se trata de una plataforma para guardar y valorar películas. Un sistema CRUD básico con opción a registro y login de usuario.

Si deseas probar la versión publicada online, puedes registrarte y acceder con tu usuario y contraseña, o bien puedes entrar con las siguientes credenciales de prueba.

test@test.com
9A@qwerty

Instalar y ejecutar Cypress para testear un proyecto real

Vamos, ahora sí, a testear esta aplicación, de modo que asegúrate de tener el proyecto levantado y corriendo en tu entorno local.

Para empezar a trabajar con Cypress, primero es necesario instalar la dependencia correspondiente. Puedes hacerlo con el siguiente comando:

Si has clonado el repositorio y seguido las instrucciones del Readme, puedes omitir este paso.

npm i cypress

Tras instalar localmente la herramienta, Cypress nos permite iniciar y configurar un entorno de testing a través de un sencillo comando.

npx cypress open

El comando anterior se encuentra integrado en un script especialmente diseñado para el proyecto en cuestión, así que en su lugar deberías ejecutar el que sigue:

npm run test

Tras hacerlo Cypress genera una serie de directorios y archivos de configuración, más adelante los analizaremos uno a uno.

Verás que a continuación también aparece una ventana parecida a esta:

Dashboard de Cypress
Dashboard de Cypress

Selecciona la opción de tests end to end, y elige uno de los navegadores disponibles.

Selección de navegador para testing con Cypress
Selección de navegador para testing con Cypress

La herramienta creará un grupo de archivos y directorios nuevos en nuestro proyecto, allí es donde programaremos nuestros tests.

Como ya supondrás Cypress es una librería muy completa, definir todas sus características en un único post es una tarea prácticamente imposible.

Así que he tratado de incluir las acciones más comunes en los tests que acompañan la app que he preparado.

Al final del post te describo en detalle cada instrucción, pero para eso, es conveniente comprender los siguientes puntos.

Puedes saltar directamente a la parte práctica, pero te recomiendo encarecidamente que leas los apartados que siguen para comprender mejor algunas peculiaridades.

Entornos de test para no alterar la base de datos

Puede que te hayas fijado en que este proyecto levanta dos instancias de frontend y backend en puertos distintos.

Una es para el entorno de desarrollo y otra para la de test.

Eso se debe a que, por lo general, no es deseable que los tests tengan un impacto en la base de datos de desarrollo (y por supuesto, mucho menos en la base de datos de producción).

Levantar dos instancias, nos permite evitar que las acciones de los tests automatizados, afecten de algún modo al proceso de desarrollo.

En este caso se crearán, editarán y eliminarán películas, y se registrarán usuarios de prueba.

Si configuraste correctamente las variables de entorno de los archivos .env.local y .env.test, los tests que programes afectarán a una base de datos independiente.

Directorios de archivos y configuración general de los tests

Para empezar a familiarizarse con Cypress, es necesario echar un vistazo a la estructura de archivos y directorios.

Por un lado, en la raíz del proyecto existe un archivo llamado cypress.config.js

Tal y como su nombre indica, se trata de un archivo de configuración, donde podemos definir algunas de las características generales de nuestros tests.

import { defineConfig } from "cypress";
import { seed } from "./backend/test-utils/testSeed.js";
export default defineConfig({
  e2e: {
    supportFile: "cypress/e2e/support/e2e.{js,jsx,ts,tsx}",
    fixturesFolder: "cypress/e2e/fixtures",
    baseUrl: `http://localhost:${process.env.PORT}`,
    env: {
      PORT: process.env.PORT,
    },
    setupNodeEvents(on, config) {
      // implement node event listeners here
      on("task", {
        async seedDatabase() {
          await seed();
          return null;
        },
      });
    },
  },
});

En este caso, mediante la función “defineConfig” se declaran las siguientes características en los tests “e2e”:

  • supportFile: Indica a Cypress donde encontrar cada grupo de tests que programamos, así como las extensiones soportadas.
  • fixturesFolder: Indica a Cypress donde encontrar los archivos “fixtures”. En el próximo párrafo te explico qué son los fixtures.
  • baseUrl: Se trata de la url base sobre la que el navegador de Cypress trabajará. De este modo, por ejemplo, cuando se indica que un test visite el apartado “/movies”, en realidad sabrá que se trata de la url `${baseUrl}/movies`. Más adelante lo veremos en un caso práctico.
  • env: En este objeto se pueden cargar variables de entorno que posteriormente poderemos recuperar mediante el método “Cypress.env(«VARIABLE»)”
  • setupNodeEvents: Cypress también permite ejecutar código de un entorno NodeJs bajo petición. Mediante el callback de setupNodeEvents, podemos declarar tareas que se encuentran a la escucha de determinados eventos producidos por los tests. En este caso, cuando se detecta el evento “seedDatabase”, se ejecuta la tarea de limpiar y rellenar de nuevo la base de datos de pruebas.

Por supuesto, el archivo de configuración de Cypress admite muchos más parámetros.

A continuación te dejo un enlace a la documentación, para que mires detenidamente qué otras opciones existen.

https://docs.cypress.io/guides/references/configuration#e2e

Sigamos con el directorio “cypress”, en él encontrarás una carpeta con el nombre “e2e” y tres subdirectorios.

  • fixtures: Cuando se realizan tests, es habitual recurrir al uso de datos de prueba, este tipo de recurso se llama “mock data”. Suele usarse como reemplazo de datos reales para realizar pruebas y comprobaciones. En el entorno de Cypress, los datos “mock” se llaman fixtures y se pueden guardar en archivos .json dentro de este directorio. Encontrarás un ejemplo en el código del repositorio.
  • specs: Aquí es donde se alojan los tests que vayamos creando. Más adelante veremos cada uno de ellos detenidamente, pero por lo pronto, debes saber que es necesario respetar la nomenclatura “nombre-del-grupo-de-tests.cy.js” al crear cualquiera de estos archivos.
  • support: Por último existen dos archivos adicionales dentro de la carpeta “support” llamados “e2e.js” y “command.js”.
    • El primero permite sobreescribir algunas de las configuraciones de “cypress.config.ts”, específicas para tests “e2e”. 
    • El segundo, permite declarar comandos personalizados, para agilizar algunas tareas repetitivas, más tarde volveremos a él para ver un caso práctico.

Diferencias entre grupos, tests y afirmaciones (Suites, tests y assertions)

En este punto ya disponemos del contexto suficiente como para sumergirnos de lleno en la creación de tests dentro del directorio “specs”.

De hecho, para hablar en propiedad, debemos referirnos a cada archivo .cy.js como una “suite” o grupo de tests.

Veamos un ejemplo sencillo de “suite” que contiene dos tests simples.

El ejemplo que sigue no pertenece al proyecto, pero nos servirá para identificar cada parte.

describe("home page", () => {
  it("the h1 contains the correct text", () => {
    cy.visit("http://localhost:3000")
    cy.get("h1").contains("Testeando aplicaciones con Cypress")
  })
  it("the h2 contains the correct text", () => {
    cy.visit("http://localhost:3000")
    cy.get("h2").contains("by libreriasjs.com")
  })
})

A pesar de que es posible que sea el primer test de Cypress que veas, estarás de acuerdo conmigo en que es bastante autoexplicativo.

Cada suite se inicia con la función “describe” seguida de una descripción y una función de callback.

En la descripción se hará referencia al área de acción del grupo de tests. En el caso del ejemplo, analizar la página home.

Seguidamente se declararán uno o más tests haciendo uso de la función “it”, de nuevo, con un parámetro de texto, y otro de callback.

En el texto especificaremos lo que se  pretende comprobar en el test en cuestión.

Por ejemplo, verificar que existe una etiqueta h1 con el texto correcto.

Dentro de la función de retorno de cada test es donde declaramos cada instrucción que Cypress debe seguir para cumplir con el test.

Existen una enorme cantidad de instrucciones a nuestra disposición. 

Para simplificar las cosas, podemos identificar cuatro tipos.

  • Query: Comandos que leen el estado de tu aplicación.
  • Assertion: Comandos que afirman el estado de la aplicación. 
  • Action: Interacciones que Cypress puede llevar a cabo con tu aplicación, como si de un usuario se trata.
  • Otros: Otras instrucciones de apoyo para facilitarte la creación de tests más elaborados.

https://docs.cypress.io/api/table-of-contents

Es importante saber que si cualquiera de estas instrucciones falla, sea por el motivo que sea, se considerará como un test fallido, y deberás investigar el motivo.

Al fin y al cabo, ese es el objetivo de un sistema de automatización de tests.

Tal y como se aprecia en el ejemplo anterior, todas las instrucciones se ejecutan a partir del objeto global “cy”.

Selectores CSS y atributos data-cy

Analizando un poco más el ejemplo anterior, vemos que se pueden seleccionar elementos del DOM utilizando el método “get”

Se trata de una “query” fundamental para interactuar con la interfaz del programa.

cy.get("h2")

Cy.get funciona recibiendo una cadena de texto en forma de selector CSS. 

Si estás habituado a usar el método “document.querySelectorAll”, estoy seguro de que te resultará fácil implementar cualquier “query”, ya que se trata, prácticamente, del mismo concepto.

Con todo, a pesar de que el ejemplo que sigue es técnicamente correcto, es poco recomendable usarlo, en seguida te cuento porqué.

cy.get(“.btn-success”)

Usar selectores CSS basados en clases, es poco consistente.

Un simple cambio de UI que afecte a la asignación de clases CSS, puede derivar en el fallo de algunos tests por no cumplir con el selector correctamente.

Es por eso que se considera una buena práctica incluir atributos especialmente pensados para usar como selectores CSS.

Un ejemplo de ello sería el siguiente. Imagina que deseas comprobar el comportamiento de un botón de la interfaz. 

Le puedes añadir un atributo data-cy=’….’ con información de su cometido:

<button data-cy=”btn-save-movie” className=”btn btn-success” onClick=”handleOnClick”>Guardar película</button>

Y luego, usar ese atributo como selector CSS para tu “query”.

cy.get(“[data-cy=’btn-save-movie’]”).click()

En esencia, solo consiste en agregar atributos que ayuden a identificar y seleccionar elementos del DOM, sin interferir en los estilos CSS aplicados.

En seguida veremos este principio aplicado en el caso práctico, pero antes hay dos características más del entorno de test, que debes conocer.

Aislamiento de los tests (o “test isolation”) y acciones pre y post Tests

Cada test declarado con la función “it()” actúa de forma completamente aislada del resto de tests.

Eso significa que el Cypress limpia el navegador eliminando todo rastro de cualquier test anterior. Desde variables de sesión, hasta datos en “caché” de cualquier tipo.

Ese comportamiento es ideal para no “contaminar” el resultado de un test anterior con una nueva comprobación.

Pero también implica repetir en cada test acciones comunes, como por ejemplo visitar la página web en cuestión.

Como dije, ese comportamiento es bueno y debemos apoyarnos en él para escribir buenos tests.

Por ese mismo motivo, además Cypress ofrece funciones para ejecutar acciones antes y/o después de cada test.

Estas funciones son “beforeEach” y “afterEach”.(“before” y “after” también existen, pero su uso es menos recomendable)

describe("Example beforeEach", () => {
  beforeEach(() => {
    cy.task("performActionInNode");
  });
});

En este ejemplo podemos ver cómo a través de beforeEach se desencadena un evento en el entorno de NodeJs.

Esto es ideal, por ejemplo, para resetear la base de datos de pruebas antes de la ejecución de cada test.

Interceptar y manejar peticiones de red

Por último, antes de entrar a analizar los tests de la aplicación, es preciso saber que Cypress también permite interceptar y analizar peticiones asíncronas que el navegador realiza.

Mediante el método “cy.intercept()” la herramienta ofrece la posibilidad de estudiar el comportamiento de una petición http

Analiza detenidamente el siguiente código:

it("should fetch data from API", () => {
  cy.intercept("GET", "/api").as("requestAPIData");
  //otros comandos cy …. 
  cy.wait("@requestAPIData")
    .its("response")
    .its("statusCode")
    .should("eq", 200);
});

En este ejemplo, verificamos que las peticiones de tipo GET que se realizan sobre la ruta “/api” del backend, deben devolver un estado 200.

Fíjate que gracias a recursos como este, podemos testear no solo el frontend, sino también partes del backend implicadas en el flujo de interacción.

Verificar el proceso de registro de nuevos usuarios.

Ahora sí, veamos la primera “Suite” de tests. 

Empezaremos con la especificación “signup”, en ella comprobaremos el correcto funcionamiento de la aplicación, durante el proceso de registro usuario.

Si haces click sobre el boton “signup” en el panel de control de Cypress, dentro del apartado “Specs” deberías ver cómo todas las instrucciones se completan de forma automática.

Video de Cypress ejecutando la suite signup

Bastante espectacular, ¿verdad?

Vamos a analizar el archivo de esta “suite”, almacenado en el directorio “cypress/e2e/specs/signup.cy.js” dentro del repositorio.

Ya conoces muchas de las instrucciones que aquí aparecen, pero permíteme destacar algunas más relevantes.

describe("SignUp", () => {
  beforeEach(() => {
    cy.task("seedDatabase");
  });
});

Antes de iniciar el test, nos aseguramos de resetear el contenido de la base de datos, para ello ejecutamos el la tarea “seedDatabase”.

Para comprender qué sucede a continuación, debemos mirar en el archivo de configuración “cypress.config.ts”.

setupNodeEvents(on, config) {
  // implement node event listeners here
  on("task", {
    async seedDatabase() {
      await seed();
      return null;
    },
  });
}

Es precisamente en setupNodeEvents, donde declaramos la función “seedDatabase” vinculado al evento “task”. 

En esa función ejecutamos el proceso de reseteo de la base de datos de prueba, definido en la función “seed()”.

Volviendo al archivo “signup.cy.js”, vemos que solo existe un único test con la descripción “should create a new user and login”.

it("should create a new user and login", async () => {
    cy.fixture("userTestCredenitals").as("userCredentials");
    cy.intercept("POST", "/signup").as("requestSignUp");
    cy.intercept("POST", "/signin").as("requestSignIn");
}

Iniciamos el test importando el fixure “userTestCredenitals.json” bajo el alias “userCredentials” para poder usarlo más tarde.

También preparamos la captura de dos peticiones POST contra el servidor, “signup” y “signin”.

cy.visit("/");
cy.location("pathname").should("eq", "/login");

Pedimos al navegador que visite la página de inicio.

Como el usuario no está autenticado, debería ser redirigido, así que comprobamos que la url equivale a “/login”.

cy.get('[data-cy="go-to-signup-btn"]').click();
cy.location("pathname").should("eq", "/sign-up");

Seleccionamos el boton con el atributo “[data-cy=»go-to-signup-btn»], y tras hacer click, verificamos si la ruta ha sido modificada acorde.

cy.get("@userCredentials").then((userCredentials) => {
  cy.get('[name="username"]').click();
  cy.get('[name="username"]').type(userCredentials.email);
  cy.get('[name="password"]').type(userCredentials.password);
  cy.get('[name="repeated-password"]').type(userCredentials.password);
});

Obtenemos el valor del fixture “userCredentials” a partir del alias (“@userCredentials”), y en la funcion de callback rellenamos los campos de nombre de usuario, contraseña y repetir contraseña.

cy.get('[data-cy="register-btn"]').click();
cy.wait("@requestSignUp")
  .its("response")
  .its("statusCode")
  .should("eq", 200);
cy.location("pathname").should("eq", "/login");

Hacemos click en el boton de registro y pedimos a Cypress que espere a que se resuelva la petición que interceptamos. Seguidamente verificamos que la respuesta sea 200.

cy.getAllLocalStorage().should("be.empty");

Esta acción de Cypress te puede sorprender un poco. 

Mediante el método “getAllLocalStorage” obtenemos toda la información almacenada en el navegador, eso nos permite verificar que aún no se ha guardado ningún dato.

cy.get("@userCredentials").then((userCredentials) => {
  cy.get('input[name="username"]').click();
  cy.get('input[name="username"]').type(userCredentials.email);
  cy.get('input[name="password"]').click();
  cy.get('input[name="password"]').type(userCredentials.password);
});
cy.get('button[type="submit"]').click();
cy.wait("@requestSignIn")
  .its("response")
  .its("statusCode")
  .should("eq", 200);
cy.location("pathname").should("eq", "/");

De nuevo rellenamos los campos de login con los datos del fixture, hacemos click en el boton de enviar, y esperamos a que se resuelva la petición capturada en el alias “@requestSignIn”.

Si todo ha ido bien, la URL ahora sí debería corresponder a “/”.

cy.getAllLocalStorage()
  .its(`http://localhost:${Cypress.env("PORT")}`)
  .its("CY_MY_MOVIES_TOKEN")
  .should("not.be.undefined");

Tras el proceso de login, comprobamos si ahora hay un token seteado en los datos del navegador.

cy.get('[data-cy="movie-card"]').should("have.length", 0);

Como se trata de un usuario completamente nuevo, el número de películas que aparece en pantalla deberia ser igual a 0.

Asi que lo comprobamos contando la cantidad de elementos con el atributo “[data-cy=»movie-card»]”.

Comprobar los procesos de login y logout.

Lanzamos la “suite” auth, haciendo click en el botón con el mismo nombre.

Video de Cypress ejecutando la suite auth

En este grupo comprobaremos tres flujos de interacción:

  • Un usuario existente se puede loguear sin problema.
  • Un usuario logueado, puede cerrar sesión.
  • Un usuario es redirigido si caduca su sesión.
  • Un usuario que no existe no puede acceder.

Tras declarar que se resetee la base de datos antes de cada test, procedemos a declarar el primero.

it("should signin", () => {
  cy.signIn();
});

En esta ocasión el test “should signin”, solo tiene una instrucción “cy.signIn()”. Se trata de un comando customizado.

Así que es necesario mirar en el archivo “cypress/e2e/support/commands.ts” para entender su lógica.

Cypress.Commands.add("signIn", () => {
  cy.intercept("POST", "/signin").as("requestSignIn");
  cy.getAllLocalStorage().should("be.empty");
  cy.visit("/");
  cy.location("pathname").should("eq", "/login");
  cy.get('input[name="username"]').click();
  cy.get('input[name="username"]').type("test@test.com");
  cy.get('input[name="password"]').click();
  cy.get('input[name="password"]').type("1A@qwertyuiop");
  cy.get('button[type="submit"]').click();
  cy.wait("@requestSignIn").its("response").its("statusCode").should("eq", 200);
  cy.location("pathname").should("eq", "/");
  cy.getAllLocalStorage()
    .its(`http://localhost:${Cypress.env("PORT")}`)
    .its("CY_MY_MOVIES_TOKEN")
    .should("not.be.undefined");
});

Muchas de las instrucciones declaradas aquí ya las hemos visto en la “suite” anterior. A grandes rasgos, se trata de comprobar los siguientes casos:

  • Se comprueba que no exista un token en localstorage.
  • Se rellenan los campos de acceso en la vista login.
  • Se comprueba que la respuesta a la petición de acceso devuelva 200.
  • Se verifica que la ruta es la de inicio (lo cual significa que el usuario no ha sido redirigido).
  • Se comprueba que el token ha sido seteado en los datos del navegador.

Usaremos el comando “cy.signIn()” así que por eso es una buena idea declararlo como una instrucción global de Cypress.

it("should signout", () => {
  cy.signIn();
  cy.get('[data-cy="signout-btn"]').click();
  cy.location("pathname").should("eq", "/login");
  cy.getAllLocalStorage().should("be.empty");
});

El siguiente test de la “suite” de auth, acciona el proceso de login para, a continuación, hacer click en el botón ‘[data-cy=»signout-btn»]’, y comprobar que el usuario ya no dispone de token de sesión.

it("should redirect after session expired", () => {
  cy.signIn();
  cy.location("pathname").should("eq", "/");
  //Sessions expired after 30min so we need to move time and stub backend response
  cy.clock();
  cy.tick(1000 * 60 * 60 * 1.5);
  cy.intercept("/api/*", {
    statusCode: 401,
    body: { error: "Unauthorized" },
  }).as("apiRequest");
  cy.get('[data-cy-movie-id="Star Wars"] [data-cy="star-btn-2"]').click();
  cy.wait("@apiRequest");
  cy.location("pathname").should("eq", "/login");
  cy.contains("Your session expired");
});

En este test se presenta una peculiaridad nueva.

Una vez logueado el usuario, se capturan las peticiones al servidor, pero en esta ocasion se fuerza una respuesta 401 (Desautorizado).

Con esto, simulamos que el token de sesión ha caducado, de modo que podemos comprobar si lo que sucede a continuación es lo que se espera.

it("should detect if user does not exists", () => {
  cy.visit("/");
  cy.get(["[]"]);
  cy.location("pathname").should("eq", "/login");
  cy.getAllLocalStorage().should("be.empty");
  cy.get('input[name="username"]').click();
  cy.get('input[name="username"]').type("no-user@test.com");
  cy.get('input[name="password"]').click();
  cy.get('input[name="password"]').type("no-user-pass");
  cy.get('button[type="submit"]').click();
  cy.contains("This user does not exists");
  cy.location("pathname").should("eq", "/login");
});

Finalmente analizamos si tras incluir unas credenciales erróneas, aparece en algun punto de la pantalla el mensaje “This user does not exists”.

Testear el CRUD de datos contra el backend.

A llegado el momento de testear el “core” de la aplicación, me refiero por supuesto a las acciones de Leer, Crear, Editar y Eliminar películas.

Video de Cypress ejecutando la suite movies

Empezamos con el proceso de verificar que existen dos películas para el usuario de prueba.

it("should get movies", () => {
  cy.signIn();
  cy.get('[data-cy="movie-card"]').should("have.length", 2);
});

Haciendo uso del método “cy.get(‘[data-cy=»movie-card»]’).should(«have.length», 2);” verificamos que existen dos elementos con el atributo identificador.

it("should rate Star Wars movie to 2", () => {
  cy.intercept("PUT", "/api/*").as("requestScoreMovie");
  cy.intercept("GET", "/api").as("requestAllMovies");
  cy.signIn();
  cy.wait("@requestAllMovies")
    .its("response")
    .its("statusCode")
    .should("eq", 200);
  cy.get('[data-cy-movie-id="Star Wars"] [data-cy="star-btn-2"]').click();
  cy.wait("@requestScoreMovie")
    .its("response")
    .its("statusCode")
    .should("eq", 200);
  cy.wait("@requestAllMovies")
    .its("response")
    .its("statusCode")
    .should("eq", 200);
  cy.get('[data-cy-movie-id="Star Wars"] [data-cy="star-btn-2"]').should(
    "have.css",
    "color",
    "rgb(255, 238, 0)"
  );
  cy.get('[data-cy-movie-id="Star Wars"] [data-cy="star-btn-3"]')
    .should("have.css", "color")
    .and("not.match", /rgb\(255, 0, 0\)/);
});

Cuando un usuario hace click en una de las estrellas de una “card”, modifica el valor, así que con el test anterior testeamos precisamente esto.

Tras hacer click al elemento del DOM que corresponde al selector ‘'[data-cy-movie-id=»Star Wars»] [data-cy=»star-btn-2″]’, podemos comprobar que en sus estilos CSS el color ha sido modificado.

Seguimos testeando el proceso de creación de una nueva película.

it("should create a new movie with name Matrix with 4 stars", () => {
  cy.intercept("POST", "/api").as("requestAddNewMovie");
  cy.signIn();
  cy.get('[data-cy="add-movie-btn"]').click();
  cy.location("pathname").should("eq", "/add-movie");
  cy.get('input[name="movieName"]').click();
  cy.get('input[name="movieName"]').type("Matrix");
  cy.get('input[name="poster"]').click();
  cy.get('input[name="poster"]').type("./matrix.jpeg");
  cy.get('[data-cy="star-btn-4"]').click();
  cy.get('[data-cy="create-movie-btn"]').click();
  cy.wait("@requestAddNewMovie")
    .its("response")
    .its("statusCode")
    .should("eq", 200);
  cy.location("pathname").should("eq", "/");
  cy.contains("Matrix");
  cy.get('[data-cy-movie-id="Matrix"]')
    .find("img")
    .should("have.attr", "src")
    .should("include", "matrix.jpeg");
  cy.get('[data-cy="movie-card"]').should("have.length", 3);
});

Con todo lo aprendido, estoy convencido de que ya estás en disposición de interpretar cada línea de código de este test. Una parte muy buena de Cypress es que su sintaxi es extremadamente sencilla de entender.

Te animo a que trates de descifrar cada instrucción de esta suite para crear tests end to end con JavaScript y Cypress.

it("should delete Star Wars movie", () => {
  cy.intercept("DELETE", "/api/*").as("requestDeleteMovie");
  cy.signIn();
  cy.get(
    '[data-cy-movie-id="Star Wars"] [data-cy="delete-movie-btn"]'
  ).click();
  cy.wait("@requestDeleteMovie")
    .its("response")
    .its("statusCode")
    .should("eq", 200);
  cy.get("[data-cy-movie-id='Star Wars']").should("not.exist");
  cy.get('[data-cy="movie-card"]').should("have.length", 1);
});

Cerramos el test agregando un test para comprobar si efectivamente la tarjeta de una película creada, desaparece tras hacer click en ‘data-cy=»delete-movie-btn»’, y esperar la respuesta del servidor.

Analizar la seguridad ante ataques XSS.

Por último, pero no menos importante, he dejado una suite que comprueba el comportamiento de la aplicación cuando un usuario trata de perpetrar un ataque de tipo XSS.

describe("Security", () => {
  beforeEach(() => {
    cy.task("seedDatabase");
  });
  it("should avoid XSS attack", () => {
    cy.intercept("POST", "/api").as("requestAddNewMovie");
    cy.signIn();
    cy.get('[data-cy="add-movie-btn"]').click();
    cy.location("pathname").should("eq", "/add-movie");
    cy.get('input[name="movieName"]').click();
    cy.get('input[name="movieName"]').type(
      "<script>console.log('XSS injected via movieName input')</script>"
    );
    cy.get('input[name="poster"]').click();
    cy.get('input[name="poster"]').type("<h1>Inject HTML code</h1>");
    cy.get('[data-cy="create-movie-btn"]').click();
    cy.wait("@requestAddNewMovie")
      .its("response")
      .its("statusCode")
      .should("eq", 500);
    cy.visit("/");
    cy.get('[data-cy="movie-card"]').should("have.length", 2);
  });
});

Otro ejemplo de la enorme versatilidad de tests que permite hacer esta librería.

Últimas observaciones y recursos adicionales

Si has llegado hasta aquí, solo me queda darte las gracias

Espero que este breve tutorial te haya ayudado a empezar con esta tecnología para crear tests end to end con JavaScript y Cypress.

Si te has quedado con ganas de testar más aplicaciones te animo a implementar Cypress en otro proyecto que desarrollé hace un tiempo:

Crea un dashboard con opción a crear capturas de pantalla

Para terminar, te dejo un listado de recursos y curiosidades sobre Cypress para que puedas seguir investigando sobre esta herramienta y el mundo del testing e2e.

Hasta la próxima.

Deja un comentario