Uso de Spies para realizar pruebas en JavaScript con Sinon.js

Sinon.js es un marco popular que se utiliza en espías de prueba independientes, stubs y simulacros para JavaScript. En este artículo, espiaremos una solicitud HTTP en una prueba unitaria.

Introducción

En las pruebas de software, un "espía" registra cómo se usa una función cuando se prueba. Esto incluye cuántas veces se llamó, si se llamó con los argumentos correctos y qué se devolvió.

Si bien las pruebas se usan principalmente para validar la salida de una función, a veces necesitamos validar cómo interactúa una función con otras partes del código.

En este artículo, profundizaremos en qué son los espías y cuándo deben usarse. Luego espiaremos una solicitud HTTP mientras usamos Sinon.js en una prueba unitaria de JavaScript.

Este artículo es el segundo de una serie sobre técnicas de prueba con Sinon.js. Te recomendamos que leas también nuestro artículo anterior:

¿Qué son los espías? {#lo que son espías}

Un espía es un objeto en pruebas que rastrea las llamadas realizadas a un método. Al rastrear sus llamadas, podemos verificar que se está utilizando de la forma en que se espera que lo use nuestra función.

Fiel a su nombre, un espía nos da detalles sobre cómo se usa una función. ¿Cuántas veces se llamó? ¿Qué argumentos se pasaron a la función?

Consideremos una función que verifica si existe un usuario y crea uno en nuestra base de datos si no existe. Podemos agregar las respuestas de la base de datos y obtener los datos de usuario correctos en nuestra prueba. Pero, ¿cómo sabemos que la función realmente está creando un usuario en los casos en que no tenemos datos de usuario preexistentes? Con un espía, observaremos cuántas veces se llama a la función crear-usuario y estaremos seguros.

Ahora que sabemos qué es un espía, pensemos en las situaciones en las que deberíamos usarlo.

¿Por qué usar espías? {#por qué los espías}

Los espías sobresalen al dar una idea del comportamiento de la función que estamos probando. Si bien validar las entradas y salidas de una prueba es crucial, examinar cómo se comporta la función puede ser crucial en muchos escenarios:

Cuando su función tiene efectos secundarios que no se reflejan en sus resultados, debe espiar los métodos que utiliza.

Un ejemplo sería una función que devuelve JSON a un usuario después de realizar muchas llamadas a varias API externas. La carga útil final de JSON no le dice al usuario cómo la función recupera todos sus datos. Un espía que monitorea cuántas veces llamó a las API externas y qué entradas usó en esas llamadas nos diría cómo.

Veamos cómo podemos usar Sinon.js para crear espías en nuestro código.

Uso de Sinon.Js para crear un espía

Hay varias formas de crear un espía con Sinon.js, cada una con sus ventajas y desventajas. Este tutorial se centrará en los siguientes dos métodos, que apuntan a espías en una sola función a la vez:

  1. Una función anónima que rastrea argumentos, valores y llamadas realizadas a un método.
  2. Un envoltorio para una función existente.

Primero, configuremos nuestro proyecto para que podamos ejecutar nuestros archivos de prueba y usar Sinon.js.

Configuración

Comencemos por crear una carpeta para almacenar nuestro código JavaScript. Cree una nueva carpeta y muévase a ella:

1
2
$ mkdir SpyTests
$ cd SpyTests

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

1
$ npm init -y

Ahora instalemos nuestras dependencias de prueba. Instalamos Moca y Chai para ejecutar nuestras pruebas, junto con Sinon.js:

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

¡Nuestra configuración está completa! Comencemos usando espías como funciones anónimas.

Espías con funciones anónimas

Como funciones anónimas, los espías de Sinon.js suelen ser útiles en los casos en los que queremos probar funciones de orden superior que toman otras funciones, es decir, devoluciones de llamada como argumentos. Veamos un ejemplo básico que vuelve a implementar Array.prototype.map() con una devolución de llamada:

Cree dos archivos, es decir, mapOperations.js y mapOperations.test.js dentro del directorio spyTests de la siguiente manera:

1
$ touch mapOperations.js mapOperations.test.js

Introduzca el siguiente código en el archivo mapOperations.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const map = (array, operation) => {
    let arrayOfMappedItems = [];
    for (let item of array) {
        arrayOfMappedItems.push(operation(item));
    }
    return arrayOfMappedItems;
};

console.log(map([{ name: 'john', role: 'author'}, { name: 'jane', role: 'owner'}], user => user.name));

module.exports = { map };

En el código anterior, map() toma una matriz como su primer argumento y una función de devolución de llamada, operation(), que transforma los elementos de la matriz como su segundo argumento.

Dentro de la función map(), iteramos a través de la matriz y aplicamos la operación en cada elemento de la matriz, luego empujamos el resultado a la matriz arrayOfMappedItems.

Cuando ejecute este ejemplo en la consola, debería obtener el siguiente resultado:

1
2
$ node mapOperations.js
[ 'john', 'jane' ]

Para probar si la función operación() fue llamada por nuestra función map(), podemos crear y pasar un espía anónimo a la función map() de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const { map } = require('./mapOperations');
const sinon = require('sinon');
const expect = require('chai').expect;

describe('test map', () => {
    const operation = sinon.spy();

    it('calls operation', () => {
        map([{ name: 'foo', role: 'author'}, { name: 'bar', role: 'owner'}], operation);
        expect(operation.called);
    });
});

Si bien nuestra devolución de llamada en realidad no transforma la matriz, nuestro espía puede verificar que la función que estamos probando realmente la usa. Esto se confirma cuando expect(operation.called); no falla la prueba.

¡Veamos si pasa nuestra prueba! Ejecute la prueba, debería obtener el siguiente resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ mocha mapOperations.test.js

  test map

     calls operation


  1 passing (4ms)

  Done in 0.58s.

¡Funciona! Ahora estamos seguros de que nuestra función utilizará cualquier devolución de llamada que pongamos en sus argumentos. Veamos ahora cómo podemos envolver una función o método usando un espía.

Espías como envoltorios de funciones o métodos {#spies como envoltorios de funciones o métodos}

En el Artículo anterior, vimos cómo podemos hacer stub de una solicitud HTTP en nuestras pruebas unitarias. Usaremos el mismo código para mostrar cómo podemos usar Sinon.js para espiar una solicitud HTTP.

En un nuevo archivo llamado index.js, agregue el siguiente código:

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

module.exports = {
    getAlbumById: async function(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));
            });
        });
    }
};

En resumen, el método getAlbumById() llama a una API JSON que obtiene una lista de fotos de un álbum cuya ID pasamos como parámetro. Anteriormente, agregamos el método request.get() para devolver una lista fija de fotos.

Esta vez, vamos a espiar el método request.get() para que podamos verificar que nuestra función hace una solicitud HTTP a la API. También verificaremos que realizó la solicitud una vez, lo cual es bueno ya que no queremos un error que envíe spam al extremo de la API.

En un nuevo archivo de prueba llamado index.test.js, escribe el siguiente código JavaScript línea por línea:

 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
const expect = require('chai').expect;
const request = require('request');
const sinon = require('sinon');
const index = require('./index');

describe('test getPhotosByAlbumId', () => {
    let requestSpy;
    before(() => {
        requestSpy = sinon.spy(request, 'get');
    });

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

    it('should getPhotosByAlbumId', (done) => {
        index.getAlbumById(2).then((photos) => {
            expect(requestSpy.calledOnce);
            expect(requestSpy.args[0][0]).to.equal("https://jsonplaceholder.typicode.com/albums/2/photos?_limit=3");
            photos.forEach(photo => {
                expect(photo).to.have.property('id');
                expect(photo).to.have.property('title');
                expect(photo).to.have.property('url');
            });
            done();
        });
    });
});

En la prueba anterior, envolvimos el método request.get() con un espía durante la instalación en la función before(). Restauramos la función cuando eliminamos la prueba en la función después().

En el caso de prueba, afirmamos que requestSpy, el objeto que rastrea el uso de request.get(), solo registra una llamada. Luego profundizamos para confirmar que su primer argumento de la llamada request.get() es la URL de la API JSON. Luego hicimos afirmaciones para asegurarnos de que las fotos devueltas tuvieran las propiedades esperadas.

Cuando ejecute la prueba, debería obtener el siguiente resultado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ mocha index.test.js


  test getPhotosByAlbumId
     should getPhotosByAlbumId (570ms)


  1 passing (587ms)

  Done in 2.53s.

Tenga en cuenta que esta prueba realizó una solicitud de red real a la API. ¡El espía envuelve alrededor de la función, no reemplaza su funcionalidad!

¡Además, los talones de prueba de Sinon.js ya son espías! Si alguna vez crea un código auxiliar de prueba, podrá ver cuántas veces se llamó y los argumentos que se pasaron a la función.

Conclusión

Un espía en pruebas nos brinda una forma de rastrear las llamadas realizadas a un método para que podamos verificar que funciona como se espera. Usamos espías para verificar si se llamó o no a un método, cuántas veces se llamó, con qué argumentos se llamó y también el valor que devolvió cuando se llamó.

En este artículo, presentamos el concepto de espías y vimos cómo podemos usar Sinon.js para crear espías. También vimos cómo podemos crear espías como funciones anónimas y cómo podemos usarlos para envolver métodos. Para casos de uso más avanzados, Sinon.js proporciona una rica API de espionaje que podemos aprovechar. Para más detalles se puede acceder a la documentación aquí.