Mock Service Worker (MSW) es una librería para interceptar peticiones desde el front que funciona con REST y GraphQL, haciendo uso de la API de Service Workers.

En muchas ocasiones, cuando empezamos a desarrollar nuestro proyecto front nos encontramos que aún no tenemos los servicios de back disponibles y necesitamos crear mocks. El uso de esta librería nos va a proporcionar una solución fácil de implementar y con muy bajo coste para interceptar y mockear las respuestas de las peticiones.

Además de poder utilizarla en el navegador, podemos implementar MSW en nuestros entornos de testing. Aunque los services workers no pueden correr fuera de un entorno de navegador web, la librería nos proporciona una solución para usar los mismos mocks que hemos usado en el navegador con nuestros tests.

¿Cuándo mockear una API?

La documentación nos va a dar 3 razones donde se puede mockear una API:

  1. Desarrollo: En muchas ocasiones, al desarrollar el front de una app aún no disponemos de los servicios de back ya que no se ha terminado su desarrollo. Crear mocks de la API es la mejor opción para poder continuar con el desarrollo.
  2. Debug: Muchas veces encontramos bugs que surgen de la interacción del front con la API, por lo que la posibilidad de hacer un mock con la respuesta que nos falla nos puede facilitar mucho la resolución del bug.
  3. Experimentar: Nos puede servir para experimentar cómo se comportará la app si decidimos cambiar ciertos casos de uso sin necesidad de reescribir partes de la app.

Instalando y creando mocks

Todos los ejemplos que se van a ver durante el post están realizados sobre una APP en React que se ha creado usando Create React App y utilizando REST en vez de GraphQL aunque la instalación es igual para usarlo con otras librerías/frameworks.

Para instalar MSW lo haremos como dependencia de desarrollo a través de npm:

npm install msw —save-dev

Una vez instalada la dependencia, el siguiente paso será crear, dentro del directorio src, una carpeta llamada mocks ‘/src/mocks’ .

Vamos a crear nuestros mocks y para ello vamos a usar el archivo teams.js donde vamos a definir 2 rutas.

/// /src/mocks/teams.js
import { rest } from 'msw';

export const teamHandlers = [
 rest.get('/teams', (req, res, ctx) => {
   return res(
     ctx.status(200),
     ctx.json({
       teams: [
         {
           id: 1,
           name: 'Real Madrid',
           arena: 'Palacio de los deportes'
         },
         {
           id: 2,
           name: 'Baskonia',
           arena: 'Buesa Arena'
         }
       ],
       pagination: {
         totalElements: 2,
         size: 10,
         page: 1
       }
     })
   );
 }),
 rest.get('/team/:id', (req, res, ctx) => {
   const { id } = req.params;
   let team;
   switch (parseInt(id)) {
     case 1:
       team = {
         coach: 'Pablo Laso',
         players: [
           { id: 1, name: 'Campazzo', position: 'G', number: 7 },
           { id: 2, name: 'Llull', position: 'G', number: 23 },
           { id: 3, name: 'Rudy', position: 'F', number: 5 },
           { id: 4, name: 'Randolph', position: 'PF', number: 3 },
           { id: 5, name: 'Tavares', position: 'C', number: 22 }
         ]
       }
       break;
     default:
       team = {
         coach: 'Dusko Ivanovic',
         players: [
           { id: 6, name: 'Vildoza', position: 'G', number: 3 },
           { id: 7, name: 'Janning', position: 'G', number: 11 },
           { id: 8, name: 'Shields', position: 'F', number: 31 },
           { id: 9, name: 'Polonara', position: 'PF', number: 33 },
           { id: 10, name: 'Eric', position: 'C', number: 50 }
         ]
       }
       break;
   }
   return res(
     ctx.status(200),
     ctx.json(team)
   );
 })
];

Lo primero que vemos es que importamos rest desde la librería. Rest es un objeto que tiene como propiedades ‘get’, ‘post’, ‘put’, ‘delete’, ‘patch’ y ‘options’ . Además, si nos fijamos bien, la forma en la que empezamos a definir nuestros mocks se parece a cómo definimos rutas con el framework Express JS.

Las propiedades del objeto rest son métodos que reciben dos parámetros:

  1. Path a la ruta del servicio.
  2. Función que tiene que retornar nuestra respuesta mock.

Siguiendo el ejemplo, vemos que nuestro primer mock será un ‘get’ al path que hemos llamado /teams.

El segundo parámetro que le pasamos es nuestra función que acepta los siguientes tres parámetros:

  1. req: Información de la petición como pueden ser los parámetros pasados por el path.
  2. res: Función que usaremos para dar nuestra respuesta mock.
  3. ctx: Un grupo de funciones que nos ayudará a definir el statusCode, headers, body, cookie, etc, de nuestra respuesta mock.

Una vez que disponemos de nuestra función, lo único que tenemos que hacer es retornar la respuesta mock que queramos. En nuestro caso, devolveremos un statusCode 200 con un JSON que va a ser el listado de 2 equipos de baloncesto y la paginación.

En el segundo mock del ejemplo, vamos a recuperar de la ruta el id a través de req.params y así devolver un mock diferente según el parámetro id que reciba.

Con esto ya habríamos creado nuestros dos primeros mock que respondería a las peticiones ‘get’ /teams y /team/:id .

Además, si lo necesitáramos, podríamos utilizar las API de sessionStorage, localStorage o indexedDB dentro de nuestra función y hacer mocks más complejos.

Integrando los mocks

Cuando tengamos creados nuestros mocks, el siguiente paso es integrarlos con nuestra app para interceptar las llamadas. Para ello, lo primero que tenemos que hacer es copiar el archivo con el worker que nos va a dar la librería. Necesitamos saber dónde se encuentra nuestro directorio /public.

Una vez que lo sepamos, haremos uso del CLI que nos proporciona MSW y lanzaremos el siguiente comando desde la terminal:

npx msw init <PUBLIC_DIR>

Este comando nos copiará el archivo con el workers en nuestra directorio public.

Ahora solo nos queda configurar el worker. He creado un archivo dentro del directorio /src/mocks que he llamado workerSetup.js.

// /src/mocks/workerSetup.js
import { setupWorker } from 'msw';
import { handlers } from './handlers';
export const server = setupWorker(...handlers);

Importamos la función setupWorker y nuestros handlers con los mocks agrupados en el archivo handlers.js.

Después, lo único que tenemos que hacer es exportar una constante que he llamado server, donde he instanciado el setupWorker pasándole como parámetro los handlers importados.

Por último, solo nos faltaría inicializar el worker para nuestro entorno de desarrollo. Para ello, dentro de app.js comprobaré si la aplicación se ha iniciado en el entorno de desarrollo y así poder iniciar el worker.

// /src/app.js
import React, { useState, useEffect } from 'react';
import { PlayerForm } from './components/PlayerForm';
import { Roster } from './components/Roster';
import { Teams } from './components/Teams';
import './App.css';

// Start Mock Server in DEV
if (process.env.NODE_ENV === 'development') {
 const { server } = require('./mocks/workerSetup');
 server.start();
}

Si se cumple la condición de que estamos en el entorno ‘development’, entonces importamos la constante server que exportamos en workerSetup.js y llamaremos a la función start() para iniciarlo.

Ahora, cualquier petición que coincida con alguna de nuestras rutas que hemos configurado en los mocks, nos devolverá como respuesta el mock correspondiente.

Integrando los mocks en nuestros test

Para poder integrar los mocks en nuestros tests tendremos que crear otra configuración diferente ya que, en entorno Node, los service workers no pueden ejecutarse.

Para la configuración en este entorno, he creado dentro de src un directorio nuevo llamado test y dentro un archivo llamado workerSetup.js que tendrá el siguiente aspecto:

// /src/test/workerSetup.js
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { handlers } from '../mocks/handlers';

const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

export { server, rest };

export { server, rest };

La principal diferencia es que ahora hemos importado setupServer desde msw/node en vez de setupWorker desde msw como habíamos hecho para el entorno de navegador.

Al igual que en el entorno de navegador, hemos creado una instancia sobre una constante que he llamado server, pasándole los mocks que creamos antes (reutilizamos los mocks previos) y exportamos server y rest.

Además, aprovechamos este archivo para que el servidor arranque antes de empezar los test, reinicie los mocks en cada test (por si hubiésemos sobrescrito algún mock) y, por último, paramos el servidor después de ejecutar todos los tests.

Al utilizar Jest como framework de testing (viene ya integrado al crear nuestra app con CRA) lo que he hecho es importar el archivo anterior en el archivo setupTest.js.

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
import './test/workerSetup';

Y con esto, ya tendríamos nuestro server mock corriendo cuando ejecutemos nuestros test.

A continuación, voy a poner un ejemplo de test donde voy a sobrescribir un mock que teníamos creado previamente para devolver un error 404 en vez de lo que teníamos configurado. Como se muestra en el código, es muy fácil cambiar el mock en momentos puntuales donde lo necesitemos.

// /src/app.test.js
import React from 'react';
import { render, waitForElement, fireEvent, screen } from '@testing-library/react';
import { server, rest } from './test/workerSetup';
import App from './App';

describe('App test', () => {
 it('renders error element', async () => {
   server.use(
     rest.get('/team/:id', (req, res, ctx) => {
       return res(ctx.status(404));
     })
   )
   const { getByText } = render(<App />);
   const [madridElement] = await waitForElement(() => [
     getByText(/Real Madrid/i),
   ]);
   fireEvent.click(madridElement);
   const [errorElement] = await waitForElement(() => [
     getByText(/Error/i)
   ]);
   expect(errorElement).toBeInTheDocument();
 });
});

He importado server y rest desde /test/workerSetup. Si nos acordamos del principio del post, había creado un mock para la ruta /team/:id que iba a devolvernos el roster de un equipo. Pero, ¿qué pasaría si queremos probar qué ocurriría si fallase la petición?

Para poder hacer este test, podemos hacer uso de server.use() y sobrescribir la respuesta mock. Para ello le pasamos como parámetro de nuevo rest.get() con el path y la respuesta que queremos sobrescribir.

Tenemos que tener cuidado ya que al usar return res(ctx.status(404)); estamos sobrescribiendo la respuesta para todos los futuros tests que hiciesen esta misma petición y daría como resultado que la petición falle al devolver un error 404.

En nuestro caso, al haber usado dentro de afterEach el método resetHandlers(), no tendríamos que preocuparnos ya que se encarga de reiniciar los mocks después de cada test.

También tenemos otra forma de sobrescribir el mock una sola vez. Podemos utilizar res.once(), y de esta forma solo reescribimos el mock para ese momento concreto que se reciba la petición y en las peticiones posteriores recibirá el mock que teníamos antes de sobrescribir.

Conclusión

MSW es una librería que, como hemos visto, es muy fácil de integrar en nuestros desarrollos y que nos aportará las siguientes ventajas:

Os invito a que le deis una oportunidad, aunque sea haciendo una pequeña prueba personal, ya que estoy seguro de que os gustará y queréis empezar a implementarla en vuestros proyectos.

En el siguiente repositorio: msw-example, podéis encontrar la app de ejemplo que he utilizado en este post para explicar la utilización de MSW.

Además, dentro encontraréis un ejemplo con una petición POST y algún test más.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.