Uso de simulacros para probar en JavaScript con Jest

Jest es un marco de prueba popular de código abierto para JavaScript. En este tutorial, usaremos Jest para simular llamadas HTTP en nuestras pruebas a través de un script de ejemplo.

Introducción

Jest es un marco de prueba popular de código abierto para JavaScript. Podemos usar Jest para crear simulacros en nuestra prueba: objetos que reemplazan objetos reales en nuestro código mientras se prueba.

En nuestra serie anterior sobre técnicas de pruebas unitarias usando Sinon.js, cubrimos cómo podemos usar Sinon.js para stub, spy y simular aplicaciones de Node.js, en particular llamadas HTTP.

En esta serie, cubriremos las técnicas de pruebas unitarias en Node.js usando Broma. Jest fue creado por Facebook y se integra bien con muchas bibliotecas y marcos de JavaScript como React, Angular y Vue, por nombrar algunos. Tiene un enfoque particular en la simplicidad y el rendimiento.

En este artículo, revisaremos qué son los simulacros y luego nos centraremos en cómo podemos configurar Jest para una aplicación Node.js para simular una llamada HTTP en nuestra prueba. Luego compararemos cómo usamos Jest y Sinon para crear simulacros para nuestros programas.

¿Qué son los simulacros? {#lo que se burla}

En las pruebas unitarias, los simulacros nos brindan la capacidad de probar la funcionalidad proporcionada por una dependencia y un medio para observar cómo nuestro código interactúa con la dependencia. Los simulacros son especialmente útiles cuando es costoso o poco práctico incluir una dependencia directamente en nuestras pruebas, por ejemplo, en los casos en que su código realiza llamadas HTTP a una API o interactúa con la capa de la base de datos.

Es preferible aislar las respuestas para estas dependencias, mientras se asegura de que se llamen según sea necesario. Aquí es donde los simulacros son útiles.

Veamos ahora cómo podemos usar Jest para crear simulacros en Node.js.

Configuración de Jest en una aplicación Node.js

En este tutorial, configuraremos una aplicación Node.js que realizará llamadas HTTP a una API JSON que contiene fotos en un álbum. Jest se usará para simular las llamadas a la API en nuestras pruebas.

Primero, vamos a crear el directorio en el que residirán nuestros archivos y movernos a él:

1
$ mkdir PhotoAlbumJest && cd PhotoAlbumJest

Luego, inicialicemos el proyecto Node con la configuración predeterminada:

1
$ npm init -y

Una vez inicializado el proyecto, procederemos a crear un archivo index.js en la raíz del directorio:

1
$ touch index.js

Para ayudarnos con las solicitudes HTTP, usaremos [Axios] (https://github.com/axios/axios).

Configuración de Axios

Usaremos axios como nuestro cliente HTTP. Axios es un cliente HTTP ligero basado en promesas para Node.js que también pueden utilizar los navegadores web. Esto hace que sea una buena opción para nuestro caso de uso.

Primero vamos a instalarlo:

1
$ npm i axios --save

Antes de usar axios, crearemos un archivo llamado axiosConfig.js a través del cual configuraremos el cliente Axios. La configuración del cliente nos permite usar configuraciones comunes en un conjunto de solicitudes HTTP.

Por ejemplo, podemos establecer encabezados de autorización para un conjunto de solicitudes HTTP o, más comúnmente, una URL base que se usará para todas las solicitudes HTTP.

Vamos a crear el archivo de configuración:

1
touch axiosConfig.js

Ahora, accedamos a axios y configuremos la URL base:

1
2
3
4
5
6
7
const axios = require('axios');

const axiosInstance = axios.default.create({
    baseURL: 'https://jsonplaceholder.typicode.com/albums'
});

module.exports = axiosInstance;

Después de configurar baseURL, hemos exportado la instancia de axios para que podamos usarla en nuestra aplicación. Usaremos www.jsonplaceholder.typicode.com, que es una API REST en línea falsa para pruebas y creación de prototipos.

En el archivo index.js que creamos anteriormente, definamos una función que devuelva una lista de URL de fotos dada la ID de un álbum:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const axios = require('./axiosConfig');

const getPhotosByAlbumId = async (id) => {
    const result = await axios.request({
        method: 'get',
        url: `/${id}/photos?_limit=3`
    });
    const { data } = result;
    return data;
};

module.exports = getPhotosByAlbumId;

Para acceder a nuestra API simplemente usamos el método axios.request() de nuestra instancia axios. Pasamos el nombre del método, que en nuestro caso es un get y la url que invocaremos.

La cadena que pasamos al campo url se concatenará a la baseURL de axiosConfig.js.

Ahora, configuremos una prueba Jest para esta función.

Configuración de Jest

Para configurar Jest, primero debemos instalar Jest como una dependencia de desarrollo usando npm:

1
$ npm i jest -D

El indicador -D es un atajo para --save-dev, que le dice a NPM que lo guarde como una dependencia de desarrollo.

Luego procederemos a crear un archivo de configuración para Jest llamado jest.config.js:

1
touch jest.config.js

Ahora, en el archivo jest.config.js, configuraremos los directorios en los que residirán nuestras pruebas:

1
2
3
4
5
6
7
module.exports = {
    testMatch: [
        '<rootDir>/**/__tests__/**/?(*.)(spec|test).js',
        '<rootDir>/**/?(*.)(spec|test).js'
    ],
    testEnvironment: 'node',
};

El valor testMatch es una matriz de patrones globales que Jest usará para detectar los archivos de prueba. En nuestro caso, estamos especificando que cualquier archivo dentro del directorio __tests__ o en cualquier parte de nuestro proyecto que tenga una extensión .spec.js o .test.js debe tratarse como un archivo de prueba.

Nota: en JavaScript, es común ver que los archivos de prueba terminan con .spec.js. Los desarrolladores usan "spec" como abreviatura de "specification". La implicación es que las pruebas contienen los requisitos funcionales o la especificación de las funciones que se implementan.

El valor testEnvironment representa el entorno en el que se ejecuta Jest, es decir, ya sea en Node.js o en el navegador. Puede leer más sobre otras opciones de configuración permitidas aquí.

Ahora modifiquemos nuestro script de prueba package.json para que use Jest como nuestro corredor de prueba:

1
2
3
"scripts": {
  "test": "jest"
},

Nuestra configuración está lista. Para probar que nuestra configuración funciona, cree un archivo de prueba en la raíz del directorio llamado index.spec.js:

1
touch index.spec.js

Ahora, dentro del archivo, escribamos una prueba:

1
2
3
4
5
describe('sum of 2 numbers', () => {
    it(' 2 + 2 equal 4', () => {
        expect(2 + 2).toEqual(4)
    });
});

Ejecute este código con el siguiente comando:

1
$ npm test

Deberías obtener este resultado:

1
2
3
4
5
6
7
8
9
 PASS  ./index.spec.js
  sum of 2 numbers
     2 + 2 equal 4 (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.897s, estimated 1s
Ran all test suites.

Con Jest configurado correctamente, ahora podemos proceder a escribir el código para simular nuestra llamada HTTP.

Burlarse de una llamada HTTP con broma {#burlarse de una llamada http con broma}

En el archivo index.spec.js, comenzaremos de nuevo, eliminando el código anterior y escribiendo un nuevo script que simulará una llamada HTTP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const axios = require('./axiosConfig');
const getPhotosByAlbumId = require('./index');

jest.mock('./axiosConfig', () => {
    return {
        baseURL: 'https://jsonplaceholder.typicode.com/albums',
        request: jest.fn().mockResolvedValue({
            data: [
                {
                    albumId: 3,
                    id: 101,
                    title: 'incidunt alias vel enim',
                    url: 'https://via.placeholder.com/600/e743b',
                    thumbnailUrl: 'https://via.placeholder.com/150/e743b'
                },
                {
                    albumId: 3,
                    id: 102,
                    title: 'eaque iste corporis tempora vero distinctio consequuntur nisi nesciunt',
                    url: 'https://via.placeholder.com/600/a393af',
                    thumbnailUrl: 'https://via.placeholder.com/150/a393af'
                },
                {
                    albumId: 3,
                    id: 103,
                    title: 'et eius nisi in ut reprehenderit labore eum',
                    url: 'https://via.placeholder.com/600/35cedf',
                    thumbnailUrl: 'https://via.placeholder.com/150/35cedf'
                }
            ]
        }),
    }
});

Aquí, primero importamos nuestras dependencias usando la sintaxis require. Como no queremos hacer ninguna llamada de red real, creamos una simulación manual de nuestro axiosConfig usando el método jest.mock(). El método jest.mock() toma la ruta del módulo como argumento y una implementación opcional del módulo como parámetro de fábrica.

Para el parámetro de fábrica, especificamos que nuestro simulacro, axiosConfig, debe devolver un objeto que consta de baseURL y request(). El baseUrl se establece en la URL base de la API. La request() es una función simulada que devuelve una serie de fotos.

La función request() que hemos definido aquí reemplaza la función real axios.request(). Cuando llamamos al método request(), nuestro método simulado será llamado en su lugar.

Lo que es importante tener en cuenta es la función jest.fn(). Devuelve una nueva función simulada, y su implementación se define entre paréntesis. Lo que hemos hecho a través de la función mockResolvedValue() es proporcionar una nueva implementación para la función request().

Por lo general, esto se hace a través de la función mockImplementation(), aunque como en realidad solo estamos devolviendo los datos que contienen nuestros resultados, podemos usar la función sugar en su lugar.

mockResolvedValue() es lo mismo que mockImplementation(() => Promise.resolve(value)).

Con un simulacro en su lugar, avancemos y escribamos una prueba:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
describe('test getPhotosByAlbumId', () => {
    afterEach(() => jest.resetAllMocks());

    it('fetches photos by album id', async () => {
        const photos = await getPhotosByAlbumId(3);
        expect(axios.request).toHaveBeenCalled();
        expect(axios.request).toHaveBeenCalledWith({ method: 'get', url: '/3/photos?_limit=3' });
        expect(photos.length).toEqual(3);
        expect(photos[0].albumId).toEqual(3)
    });
});

Después de cada caso de prueba, nos aseguramos de que se llame a la función jest.resetAllMocks() para restablecer el estado de todos los simulacros.

En nuestro caso de prueba, llamamos a la función getPhotosByAlbumId() con un ID de 3 como argumento. Entonces hacemos nuestras afirmaciones.

La primera afirmación espera que se haya llamado al método axios.request(), mientras que la segunda afirmación verifica que el método se haya llamado con los parámetros correctos. También verificamos que la longitud de la matriz devuelta sea 3 y que el primer objeto de la matriz tenga un albumId de 3.

Vamos a ejecutar nuestras nuevas pruebas con:

1
npm test

Deberíamos obtener el siguiente resultado:

1
2
3
4
5
6
7
8
9
PASS  ./index.spec.js
  test getPhotosByAlbumId
     fetches photos by album id (7ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.853s, estimated 1s
Ran all test suites.

Con esta nueva familiaridad y experiencia, hagamos una comparación rápida de las experiencias de prueba con Jest y Sinon, que también se usa comúnmente para burlarse.

Sinon Mocks vs Jest Mocks

Sinon.js y Jest tienen diferentes formas de abordar el concepto de burla. Las siguientes son algunas de las diferencias clave a tener en cuenta:

  • En Jest, los módulos de Node.js se simulan automáticamente en sus pruebas cuando coloca los archivos simulados en una carpeta __mocks__ que está al lado de la carpeta node_modules. Por ejemplo, si tiene un archivo llamado __mock__/fs.js, entonces cada vez que se llame al módulo fs en su prueba, Jest usará automáticamente los simulacros. Por otro lado, con Sinon.js debes simular cada dependencia manualmente usando el método sinon.mock() en cada prueba que lo necesite.
  • En Jest, usamos el método jest.fn().mockImplementation() para reemplazar la implementación de una función simulada con una respuesta añadida. Un buen ejemplo de esto se puede encontrar en la documentación de Jest [aquí](https://jestjs.io/docs/en/es6-class-mocks#calling-jestmockdocsenjest-objectjestmockmodulename-factory-options-with-the-module- parámetro de fábrica). En Sinon.js, usamos el método mock.expects() para manejar eso.
  • Jest proporciona una gran cantidad de métodos para trabajar con su API simulada y particularmente con módulos. Puedes verlos todos aquí. Sinon.js, por otro lado, tiene menos métodos para trabajar con simulacros y expone una API generalmente más simple.
  • Los simulacros de Sinon.js se envían como parte de la biblioteca Sinon.js, que se puede conectar y usar en combinación con otros marcos de prueba como Mocha y bibliotecas de afirmación como Chai. Los simulacros de Jest, por otro lado, se envían como parte del marco Jest, que también se envía con su propia API de aserciones.

Los simulacros de Sinon.js a menudo son más beneficiosos cuando estás probando una aplicación pequeña que puede no requerir todo el poder de un marco como Jest. También es útil cuando ya tiene una configuración de prueba y necesita agregar simulación a algunos componentes en su aplicación.

Sin embargo, cuando se trabaja con aplicaciones grandes que tienen muchas dependencias, aprovechar el poder de la API simulada de Jest junto con su marco puede ser muy beneficioso para garantizar una experiencia de prueba consistente.

Conclusión

En este artículo, hemos visto cómo podemos usar Jest para simular una llamada HTTP realizada con axios. Primero configuramos la aplicación para usar axios como nuestra biblioteca de solicitudes HTTP y luego configuramos Jest para ayudarnos con las pruebas unitarias. Finalmente, hemos revisado algunas diferencias entre los simulacros de Sinon.js y Jest y cuándo podemos emplearlos mejor.

Para leer más sobre los simulacros de Jest y cómo puede aprovecharlos para casos de uso más avanzados, consulte su documentación aquí.

Como siempre, el código de este tutorial se puede encontrar en GitHub.