Mirage JS Deep Dive: uso de Mirage JS y Cypress para pruebas de interfaz de usuario (parte 4)

Publicado: 2022-03-10
Resumen rápido ↬ En esta parte final de la serie Mirage JS Deep Dive, aplicaremos todo lo que aprendimos en la serie anterior para aprender a realizar pruebas de interfaz de usuario con Mirage JS.

Una de mis citas favoritas sobre las pruebas de software es de la documentación de Flutter. Dice:

“¿Cómo puede asegurarse de que su aplicación continúe funcionando a medida que agrega más funciones o cambia la funcionalidad existente? Escribiendo pruebas.”

En ese sentido, esta última parte de la serie Mirage JS Deep Dive se centrará en el uso de Mirage para probar su aplicación de front-end de JavaScript.

Nota : este artículo asume un entorno Cypress. Cypress es un marco de prueba para pruebas de interfaz de usuario. Sin embargo, puede transferir el conocimiento aquí a cualquier entorno o marco de prueba de interfaz de usuario que utilice.

Leer partes anteriores de la serie:

  • Parte 1: Comprensión de los modelos y asociaciones de Mirage JS
  • Parte 2: Comprender las fábricas, los accesorios y los serializadores
  • Parte 3: Comprender el tiempo, la respuesta y el traspaso

Introducción a las pruebas de interfaz de usuario

La prueba de interfaz de usuario o interfaz de usuario es una forma de prueba de aceptación que se realiza para verificar los flujos de usuario de su aplicación front-end. El énfasis de este tipo de pruebas de software está en el usuario final, que es la persona real que interactuará con su aplicación web en una variedad de dispositivos que van desde computadoras de escritorio, portátiles hasta dispositivos móviles. Estos usuarios estarían conectados o interactuando con su aplicación utilizando dispositivos de entrada como un teclado, un mouse o pantallas táctiles. Las pruebas de IU, por lo tanto, están escritas para imitar la interacción del usuario con su aplicación lo más cerca posible.

Tomemos un sitio web de comercio electrónico, por ejemplo. Un escenario típico de prueba de IU sería:

  • El usuario puede ver la lista de productos cuando visita la página de inicio.

Otros escenarios de prueba de IU podrían ser:

  • El usuario puede ver el nombre de un producto en la página de detalles del producto.
  • El usuario puede hacer clic en el botón “añadir al carrito”.
  • El usuario puede pagar.

Captas la idea ¿cierto?

Al realizar pruebas de interfaz de usuario, se basará principalmente en los estados de back-end, es decir, ¿devolvió los productos o hubo un error? El papel que juega Mirage en esto es hacer que esos estados del servidor estén disponibles para que los modifique según lo necesite. Entonces, en lugar de realizar una solicitud real a su servidor de producción en sus pruebas de interfaz de usuario, realiza la solicitud al servidor simulado de Mirage.

Para la parte restante de este artículo, realizaremos pruebas de IU en una IU de aplicación web de comercio electrónico ficticia. Entonces empecemos.

¡Más después del salto! Continúe leyendo a continuación ↓

Nuestra primera prueba de IU

Como se indicó anteriormente, este artículo asume un entorno Cypress. Cypress hace que probar la interfaz de usuario en la web sea rápido y fácil. Puede simular clics y navegación y puede visitar rutas mediante programación en su aplicación. Consulte los documentos para obtener más información sobre Cypress.

Entonces, suponiendo que Cypress y Mirage estén disponibles para nosotros, comencemos definiendo una función de proxy para su solicitud de API. Podemos hacerlo en el archivo support/index.js de nuestra configuración de Cypress. Simplemente pegue el siguiente código en:

 // cypress/support/index.js Cypress.on("window:before:load", (win) => { win.handleFromCypress = function (request) { return fetch(request.url, { method: request.method, headers: request.requestHeaders, body: request.requestBody, }).then((res) => { let content = res.headers.map["content-type"] === "application/json" ? res.json() : res.text() return new Promise((resolve) => { content.then((body) => resolve([res.status, res.headers, body])) }) }) } })

Luego, en el archivo de arranque de su aplicación ( main.js para Vue, index.js para React), usaremos Mirage para enviar las solicitudes de API de su aplicación a la función handleFromCypress solo cuando se esté ejecutando Cypress. Aquí está el código para eso:

 import { Server, Response } from "miragejs" if (window.Cypress) { new Server({ environment: "test", routes() { let methods = ["get", "put", "patch", "post", "delete"] methods.forEach((method) => { this[method]("/*", async (schema, request) => { let [status, headers, body] = await window.handleFromCypress(request) return new Response(status, headers, body) }) }) }, }) }

Con esa configuración, cada vez que se ejecuta Cypress, su aplicación sabe usar Mirage como el servidor simulado para todas las solicitudes de API.

Sigamos escribiendo algunas pruebas de IU. Comenzaremos probando nuestra página de inicio para ver si muestra 5 productos . Para hacer esto en Cypress, necesitamos crear un archivo homepage.test.js en la carpeta de tests en la raíz del directorio de su proyecto. A continuación, le diremos a Cypress que haga lo siguiente:

  • Visite la página de inicio es decir / ruta
  • Luego afirma si tiene li elementos con la clase de product y también verifica si son 5 en números.

Aquí está el código:

 // homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Es posible que haya adivinado que esta prueba fallaría porque no tenemos un servidor de producción que devuelva 5 productos a nuestra aplicación de front-end. ¿Asi que que hacemos? ¡Nos burlamos del servidor en Mirage! Si traemos Mirage, puede interceptar todas las llamadas de red en nuestras pruebas. Hagamos esto a continuación e iniciemos el servidor Mirage antes de cada prueba en la función beforeEach y también apáguelo en la función afterEach . Las funciones beforeEach y afterEach son proporcionadas por Cypress y se pusieron a disposición para que pueda ejecutar el código antes y después de cada ejecución de prueba en su conjunto de pruebas, de ahí el nombre. Así que veamos el código para esto:

 // homepage.test.js import { Server } from "miragejs" let server beforeEach(() => { server = new Server() }) afterEach(() => { server.shutdown() }) it("shows the products", function () { cy.visit("/") cy.get("li.product").should("have.length", 5) })

Bien, estamos llegando a alguna parte; hemos importado el servidor de Mirage y lo estamos iniciando y apagando en las funciones beforeEach y afterEach respectivamente. Vamos a burlarnos de nuestro recurso de producto.

 // homepage.test.js import { Server, Model } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Nota : siempre puede echar un vistazo a las partes anteriores de esta serie si no comprende las partes de Mirage del fragmento de código anterior.

  • Parte 1: Comprensión de los modelos y asociaciones de Mirage JS
  • Parte 2: Comprender las fábricas, los accesorios y los serializadores
  • Parte 3: Comprender el tiempo, la respuesta y el traspaso

De acuerdo, comenzamos a desarrollar nuestra instancia de servidor creando el modelo de producto y también creando el controlador de ruta para la ruta /api/products . Sin embargo, si ejecutamos nuestras pruebas, fallará porque aún no tenemos ningún producto en la base de datos de Mirage.

Completemos la base de datos de Mirage con algunos productos. Para hacer esto, podríamos haber usado el método create() en nuestra instancia de servidor, pero crear 5 productos a mano parece bastante tedioso. Debería haber una mejor manera.

Ah, sí, lo hay. Utilicemos fábricas (como se explica en la segunda parte de esta serie). Tendremos que crear nuestra fábrica de productos así:

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Luego, finalmente, usaremos createList() para crear rápidamente los 5 productos que nuestra prueba debe pasar.

Hagámoslo:

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList("product", 5) cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Entonces, cuando ejecutamos nuestra prueba, ¡pasa!

Nota : después de cada prueba, el servidor de Mirage se apaga y se reinicia, por lo que nada de este estado se filtrará en las pruebas.

Evitar varios servidores Mirage

Si ha estado siguiendo esta serie, notará cuando estábamos usando Mirage en desarrollo para interceptar nuestras solicitudes de red; teníamos un archivo server.js en la raíz de nuestra aplicación donde configuramos Mirage. En el espíritu de DRY (Don't Repeat Yourself), creo que sería bueno utilizar esa instancia de servidor en lugar de tener dos instancias separadas de Mirage para desarrollo y prueba. Para hacer esto (en caso de que aún no tenga un archivo server.js ), simplemente cree uno en el directorio src de su proyecto.

Nota : su estructura diferirá si está utilizando un marco de JavaScript, pero la idea general es configurar el archivo server.js en la raíz src de su proyecto.

Entonces, con esta nueva estructura, exportaremos una función en server.js que es responsable de crear nuestra instancia de servidor Mirage. Vamos a hacer eso:

 // src/server.js export function makeServer() { /* Mirage code goes here */}

Completemos la implementación de la función makeServer eliminando el servidor Mirage JS que creamos en homepage.test.js y agregándolo al cuerpo de la función makeServer :

 import { Server, Model, Factory } from 'miragejs'; export function makeServer() { let server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

Ahora todo lo que tiene que hacer es importar makeServer en su prueba. El uso de una sola instancia de Mirage Server es más limpio; De esta forma, no tiene que mantener dos instancias de servidor para los entornos de desarrollo y prueba.

Después de importar la función makeServer , nuestra prueba ahora debería verse así:

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer(); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Así que ahora tenemos un servidor central de Mirage que nos sirve tanto para el desarrollo como para las pruebas. También puede utilizar la función makeServer para iniciar el desarrollo de Mirage (consulte la primera parte de esta serie).

Su código de Mirage no debería entrar en producción. Por lo tanto, dependiendo de la configuración de su compilación, solo necesitará iniciar Mirage durante el modo de desarrollo.

Nota : lea mi artículo sobre cómo configurar API Mocking con Mirage y Vue.js para ver cómo lo hice en Vue para que pueda replicar en cualquier marco de front-end que use.

Entorno de prueba

Mirage tiene dos entornos: desarrollo (predeterminado) y prueba . En el modo de desarrollo, el servidor de Mirage tendrá un tiempo de respuesta predeterminado de 400 ms (que puede personalizar. Consulte el tercer artículo de esta serie), registra todas las respuestas del servidor en la consola y carga las semillas de desarrollo.

Sin embargo, en el entorno de prueba, tenemos:

  • 0 retrasos para mantener nuestras pruebas rápidas
  • Mirage suprime todos los registros para no contaminar sus registros de CI
  • Mirage también ignorará la función de seeds() para que sus datos de semillas se puedan usar únicamente para el desarrollo, pero no se filtren en sus pruebas. Esto ayuda a mantener sus pruebas deterministas.

Actualicemos nuestro makeServer para poder beneficiarnos del entorno de prueba. Para hacer eso, haremos que acepte un objeto con la opción de entorno (lo pondremos por defecto en desarrollo y lo anularemos en nuestra prueba). Nuestro server.js ahora debería verse así:

 // src/server.js import { Server, Model, Factory } from 'miragejs'; export function makeServer({ environment = 'development' } = {}) { let server = new Server({ environment, models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

También tenga en cuenta que estamos pasando la opción de entorno a la instancia del servidor Mirage usando la abreviatura de propiedad ES6. Ahora, con esto en su lugar, actualicemos nuestra prueba para anular el valor del entorno para probar. Nuestra prueba ahora se ve así:

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Pruebas AAA

Mirage fomenta un estándar para las pruebas denominado enfoque de pruebas triple A o AAA. Esto significa Organizar , Actuar y Afirmar . Ya podría ver esta estructura en nuestra prueba anterior:

 it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })

Es posible que deba romper este patrón, pero 9 de cada 10 veces debería funcionar bien para sus pruebas.

Probemos los errores

Hasta ahora, hemos probado nuestra página de inicio para ver si tiene 5 productos, sin embargo, ¿qué pasa si el servidor no funciona o algo salió mal al buscar los productos? No necesitamos esperar a que el servidor esté inactivo para trabajar en cómo se vería nuestra interfaz de usuario en tal caso. Simplemente podemos simular ese escenario con Mirage.

Devolvamos un 500 (Error del servidor) cuando el usuario está en la página de inicio. Como hemos visto en un artículo anterior, para personalizar las respuestas de Mirage hacemos uso de la clase Response. Importémoslo y escribamos nuestra prueba.

 homepage.test.js import { Response } from "miragejs" it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

¡Qué mundo de flexibilidad! Simplemente anulamos la respuesta que Mirage devolvería para probar cómo se mostraría nuestra interfaz de usuario si fallara al obtener productos. Nuestro archivo general homepage.test.js ahora se vería así:

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

Tenga en cuenta que la modificación que hicimos en el controlador /api/products solo vive en nuestra prueba. Eso significa que funciona como definimos anteriormente cuando está en modo de desarrollo.

Entonces, cuando ejecutamos nuestras pruebas, ambos deberían pasar.

Nota : creo que vale la pena señalar que los elementos que estamos consultando en Cypress deberían existir en la interfaz de usuario de su interfaz de usuario. Cypress no crea elementos HTML por usted.

Prueba de la página de detalles del producto

Finalmente, probemos la interfaz de usuario de la página de detalles del producto. Así que esto es lo que estamos probando:

  • El usuario puede ver el nombre del producto en la página de detalles del producto

Hagámoslo. Primero, creamos una nueva prueba para probar este flujo de usuario.

Aquí está la prueba:

 it("shows the product's name on the detail route", function() { let product = this.server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

Su homepage.test.js finalmente debería verse así.

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { console.log(server); server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); }); it("shows the product's name on the detail route", function() { let product = server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

Cuando ejecute sus pruebas, las tres deberían pasar.

Terminando

Ha sido divertido mostrarte el interior de Mirage JS en esta serie. Espero que haya estado mejor equipado para comenzar a tener una mejor experiencia de desarrollo front-end al usar Mirage para simular su servidor back-end. También espero que utilice el conocimiento de este artículo para escribir más pruebas de aceptación/interfaz de usuario/extremo a extremo para sus aplicaciones front-end.

  • Parte 1: Comprensión de los modelos y asociaciones de Mirage JS
  • Parte 2: Comprender las fábricas, los accesorios y los serializadores
  • Parte 3: Comprender el tiempo, la respuesta y el traspaso
  • Parte 4: uso de Mirage JS y Cypress para pruebas de interfaz de usuario