Uso de stubs para pruebas en JavaScript con Sinon.js

Es posible que no tengamos acceso a los servicios que usamos mientras probamos el software en el trabajo. Para garantizar las pruebas adecuadas en todos los entornos, usaremos Sinon.js para crear pruebas unitarias que cubran las solicitudes HTTP.

Introducción

Las pruebas son una parte fundamental del proceso de desarrollo de software. Al crear aplicaciones web, hacemos llamadas a API, bases de datos u otros servicios de terceros en nuestro entorno. Por lo tanto, nuestras pruebas deben validar que las solicitudes se envíen y las respuestas se manejen correctamente. Sin embargo, es posible que no siempre podamos comunicarnos con esos servicios externos cuando realizamos pruebas.

En nuestra computadora de desarrollo local, es posible que no tengamos las claves API de la empresa o las credenciales de la base de datos para ejecutar una prueba con éxito. Es por eso que a veces "falsificamos" las respuestas HTTP o de la base de datos con un stub, engañando a nuestro código para que se comporte como si se hubiera realizado una solicitud real.

En este artículo, comenzaremos analizando qué son los stubs y por qué querríamos usarlos. A continuación, aprovecharemos Sinon.js, una popular biblioteca de pruebas de JavaScript, para crear pruebas unitarias para JavaScript que apunten a una solicitud HTTP.

Luego, daremos seguimiento a esto con artículos sobre espías y simulacros:

¿Qué son los talones?

Un stub de prueba es una función u objeto que reemplaza el comportamiento real de un módulo con una respuesta fija. El stub solo puede devolver la respuesta fija para la que fue programado.

Un stub puede verse como una suposición para nuestra prueba: si asumimos que un servicio externo devuelve esta respuesta, así es como se comportará la función.

Imagine que tiene una función que acepta una solicitud HTTP y obtiene datos de un punto final de GraphQL. Si no podemos conectarnos al punto final de GraphQL en nuestras pruebas, agregaríamos su respuesta para que nuestro código se ejecute como si GraphQL realmente hubiera sido alcanzado. Nuestro código de función no conocería la diferencia entre una respuesta GraphQL real y nuestra respuesta añadida.

Veamos escenarios en los que el stubbing es útil.

¿Por qué usar talones?

Al realizar solicitudes a servicios externos en una prueba, puede encontrarse con estos problemas:

  • Pruebas fallidas debido a errores de conectividad de red en lugar de errores de código
  • Tiempos de ejecución prolongados ya que la latencia de la red se suma al tiempo de prueba
  • Afectar erróneamente los datos de producción con pruebas si ocurre un error de configuración

Podemos sortear estos problemas aislando nuestras pruebas y bloqueando estas llamadas de servicio externo. No habría dependencia de la red, lo que haría que nuestras pruebas fueran más predecibles y menos propensas a fallar. Sin latencia de red, se espera que nuestras pruebas también sean más rápidas.

Hay escenarios en los que las solicitudes externas no funcionarían. Por ejemplo, es común en los procesos de compilación de CI/CD bloquear solicitudes externas mientras se ejecutan pruebas por razones de seguridad. También es probable que en algún momento escribamos código que dependa de un servicio que todavía está en desarrollo y no en un estado para ser utilizado.

En estos casos, los stubs son muy útiles ya que nos permiten probar nuestro código incluso cuando el servicio no está disponible.

Ahora que sabemos qué son los stubs y por qué son útiles, usemos Sinon.js para obtener experiencia práctica con los stubs.

Usando Sinon.js para crear un stub

Usaremos Sinon.js para agregar una respuesta de una API JSON que recupera una lista de fotos en un álbum. Nuestras pruebas se crearán con las bibliotecas de prueba Moca y Chai. Si desea obtener más información sobre las pruebas con Mocha y Chai antes de continuar, puede seguir Nuestra guía.

Configuración

Primero, en su terminal, cree una nueva carpeta y acceda a ella:

1
2
$ mkdir PhotoAlbum
$ cd PhotoAlbum

Inicialice NPM para que pueda realizar un seguimiento de los paquetes que instala:

1
$ npm init -y

Una vez que esté completo, podemos comenzar a instalar nuestras dependencias. Primero, instalemos la biblioteca solicitud, que será utilizada por nuestro código para crear una solicitud HTTP a la API. En tu terminal, ingresa:

1
$ npm i request --save

Ahora, instalemos todas las bibliotecas de prueba como dependencias de desarrollo. El código de prueba no se usa en producción, por lo que no instalamos bibliotecas de prueba como dependencias de código normales con la opción --save. En su lugar, usaremos la opción --save-dev para decirle a NPM que estas dependencias solo deben usarse en nuestro entorno de desarrollo/prueba. Introduce el comando en tu terminal:

1
$ npm i mocha chai sinon --save-dev

Con todas nuestras bibliotecas importadas, crearemos un nuevo archivo index.js y agregaremos el código para realizar la solicitud API allí. Puede usar la terminal para crear el archivo index.js:

1
$ touch index.js

En su editor de texto o IDE, escriba el código a continuación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const request = require('request');

const getPhotosByAlbumId = (id) => {
    const requestUrl = `https://jsonplaceholder.typicode.com/albums/${id}/photos?_limit=3`;
    return new Promise((resolve, reject) => {
        request.get(requestUrl, (err, res, body) => {
            if (err) {
                return reject(err);
            }
            resolve(JSON.parse(body));
        });
    });
};

module.exports = getPhotosByAlbumId;

Esta función realiza una llamada a una API que devuelve una lista de fotos de un álbum cuyo ID se pasa como parámetro a la función. Limitamos la respuesta a solo devolver tres fotos.

Ahora escribiremos pruebas para nuestra función para confirmar que funciona como se esperaba. Nuestra primera prueba no usará stubs, sino que hará la solicitud real.

Pruebas sin talones

Primero, creemos un archivo para escribir nuestras pruebas. En la terminal o de otro modo, crea un archivo index.test.js en el directorio actual:

1
$ touch index.test.js

Nuestro código probará que obtenemos tres fotos y que cada foto tiene las propiedades id, title y url esperadas.

En el archivo index.test.js, agregue el siguiente código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const expect = require('chai').expect;
const getPhotosByAlbumId = require('./index');

describe('withoutStub: getPhotosByAlbumId', () => {
    it('should getPhotosByAlbumId', (done) => {
        getPhotosByAlbumId(1).then((photos) => {
            expect(photos.length).to.equal(3);
            photos.forEach(photo => {
                expect(photo).to.have.property('id');
                expect(photo).to.have.property('title');
                expect(photo).to.have.property('url');
            });
            done();
        });
    });
});

En esta prueba, primero requerimos la función expect() de Chai, y luego requerimos la función getPhotosByAlbumId() de nuestro archivo index.js.

Usamos las funciones describe() y it() de Mocha para poder usar el comando mocha para ejecutar el código como prueba.

Antes de ejecutar nuestra prueba, debemos agregar un script a nuestro paquete.json para ejecutar nuestras pruebas. En el archivo package.json, agregue lo siguiente:

1
2
3
"scripts": {
    "test": "mocha index.test.js"
}

Ahora ejecute su prueba con el siguiente comando:

1
$ npm test

Debería ver esta salida:

1
2
3
4
5
6
$ mocha index.test.js

  withoutStub: getPhotosByAlbumId
     should getPhotosByAlbumId (311ms)

  1 passing (326ms)

En este caso, la prueba tardó 326 ms en ejecutarse, sin embargo, eso puede variar según la velocidad de Internet y la ubicación.

Esta prueba no pasaría si no tiene una conexión a Internet activa, ya que la solicitud HTTP fallaría. Aunque eso no quiere decir que la función no se comporte como se esperaba. Usemos un stub para que podamos probar el comportamiento de nuestra función sin una dependencia de la red.

Pruebas con stubs

Reescribamos nuestra función para que podamos enviar la solicitud a la API, devolviendo una lista predefinida de fotos:

 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const expect = require('chai').expect;
const request = require('request');
const sinon = require('sinon');
const getPhotosByAlbumId = require('./index');

describe('with Stub: getPhotosByAlbumId', () => {
    before(() => {
        sinon.stub(request, 'get')
            .yields(null, null, JSON.stringify([
                {
                    "albumId": 1,
                    "id": 1,
                    "title": "accusamus beatae ad facilis cum similique qui sunt",
                    "url": "https://via.placeholder.com/600/92c952",
                    "thumbnailUrl": "https://via.placeholder.com/150/92c952"
                },
                {
                    "albumId": 1,
                    "id": 2,
                    "title": "reprehenderit est deserunt velit ipsam",
                    "url": "https://via.placeholder.com/600/771796",
                    "thumbnailUrl": "https://via.placeholder.com/150/771796"
                },
                {
                    "albumId": 1,
                    "id": 3,
                    "title": "officia porro iure quia iusto qui ipsa ut modi",
                    "url": "https://via.placeholder.com/600/24f355",
                    "thumbnailUrl": "https://via.placeholder.com/150/24f355"
                }
            ]));
    });

    after(() => {
        request.get.restore();
    });

    it('should getPhotosByAlbumId', (done) => {
        getPhotosByAlbumId(1).then((photos) => {
            expect(photos.length).to.equal(3);
            photos.forEach(photo => {
                expect(photo).to.have.property('id');
                expect(photo).to.have.property('title');
                expect(photo).to.have.property('url');
            });
            done();
        });
    });
});

Antes de que se ejecute la prueba, le decimos a Sinon.js que bloquee la función get() del objeto request que se usa en getPhotosByAlbumId ().

Los argumentos pasados ​​a la función yields() del stub son los argumentos que se pasarán a la devolución de llamada de la solicitud de obtención. Pasamos null para los parámetros err y res, y una serie de datos de álbumes de fotos falsos para el parámetro body.

Nota: La función after() se ejecuta después de completar una prueba. En este caso restauramos el comportamiento de la función get() de la biblioteca request. Las mejores prácticas fomentan que nuestros estados de prueba sean independientes para cada prueba. Al restaurar la función, los cambios que hicimos para esta prueba no afectarían la forma en que se usa en otras pruebas.

Como antes, ejecutamos esta prueba con npm test. Debería ver el siguiente resultado:

1
2
3
4
5
6
$ mocha index.test.js

  with Stub: getPhotosByAlbumId
     should getPhotosByAlbumId

  1 passing (37ms)

¡Excelente! Ahora sin conexión a Internet, todavía estamos seguros de que nuestra función funciona bien con los datos esperados. ¡La prueba también fue más rápida! Sin una solicitud de red, simplemente necesitamos obtener los datos de la memoria.

Conclusión

Un stub es un reemplazo de una función que devuelve datos fijos cuando se le llama. Por lo general, conectamos solicitudes a sistemas externos para hacer que las ejecuciones de prueba sean más predecibles y eliminar la necesidad de conexiones de red.

Sinon.js se puede usar junto con otros marcos de prueba para funciones de código auxiliar. En este artículo, agregamos una solicitud HTTP GET para que nuestra prueba pueda ejecutarse sin una conexión a Internet. También redujo el tiempo de prueba.

Si desea ver el código de este tutorial, puede encontrarlo aquí.

In our next article, we continue on with Sinon.js and cover cómo usar espías para probar JavaScript. .