Seleccionar página

Esta es una versión actualizada del artículo Test de un API REST con Mocha, Chai, Co y Fetch

Uno de los contextos donde puede llegar a ser más útil el uso de async/await para gestionar la asincronía de Javascript es en los juegos de pruebas, ya que es habitual tener que realizar varios pasos uno detrás de otro e ir encadenando las acciones. En estos casos las llamadas con callbacks producen que la indentación del código vaya creciendo rápidamente haciendo nuestro código difícil de leer y comprender.

A continuación vamos ver cómo crear un conjunto de pruebas de un API REST fácilmente comprensibles con mocha como gestor de los test, chai como librería de comprobación, fetch para llamar al servidor por HTTP y async / await para gestionar las llamadas asíncronas.

Introducción

Vamos a hacer una pequeña introducción sobre cada una de los elementos que vamos a utilizar. Si ya los conoces, puedes pasar directamente a la siguiente sección donde mostramos ejemplos concretos de su utilización.

Mocha

Es un popular gestor de tests en Javascript que funciona tanto en Node como en los navegadores. Su instalación es sencilla si disponemos por medio de NPM:

npm install mocha --save-dev

Para crear una test debemos encadenar llamadas a describe() para los grupos e incluir llamadas a it() para cada una de las pruebas. El callback de cada it() recibe una función done que ejecutaremos cuando hayamos realizado todas las comprobaciones.

Este es un ejemplo básico de Mocha:

describe('Array', function() {
  describe('#indexOf()', function() {
    it('Debe devolver -1 cuando el valor no está en la matriz', function(done) {
      console.assert(-1, [1,2,3].indexOf(4));
      done();
    });
  });
});

Para ejecutar el test debemos incluir este código en un fichero y utilizar la siguiente línea de comando (puede variar dependiendo de donde tengamos instalado Mocha, sobre todo si lo hemos instalado globalmente):

./node_modules/.bin/mocha test01.js

Mocha dispone de muchas más funcionalidades, pero con esta introducción es suficiente para avanzar. Si quieres saber más, te recomendamos que consultes: https://mochajs.org/.

Chai

Para hacer las comprobaciones en los tests podemos usar un sencillo console.assert(), como hemos utilizado en nuestro ejemplo anterior, también podemos usar el módulo assert que provee directamente Node, pero es bastante práctico utilizar una librería de comprobación avanzada como es Chai, que podemos instalar con:

npm install chai --save-dev

Chai dispone de varios interfaces con diferentes estilos de comprobación. Esto es muy práctico para utilizar el que más nos guste, pero suele confundir bastante a los que están empezado. Nosotros hemos elegido expect, no es ni mejor ni peor que las demás, simplemente es la que hemos utilizado.

Vamos a modificar nuestro ejemplo anterior para utilizar Chai:

const expect = require('chai').expect;

describe('Array', function() {
  describe('#indexOf()', function() {
    it('Debe devolver -1 cuando el valor no está en la matriz', function(done) {
      expect([1,2,3].indexOf(4)).to.be.a('Number');
      expect([1,2,3].indexOf(4)).to.be.equal(-1);
      done();
    });
  });
});

Hemos utilizado dos de los métodos que nos ofrece Chai para hacer las comprobaciones: to.be.a() o to.be.an() que permite comprobar tipos y to.be.equal() que permite comprobar valores. También es útil conocer to.be.deep.equal() para comprobar que dos objetos tienen los mismos valores en todas sus propiedades.

Chai ofrece muchas opciones y métodos para hacer comprobaciones de todo tipo. Si quieres conocerlas, consulta http://chaijs.com/.

Fetch

Hay muchos paquetes para gestionar llamadas HTTP, pero la aparición de estándar fetch nos anima a utilizar esta solución, que funciona tanto en navegadores como en Node. Te recomendamos que leas nuestro artículo API fetch, el nuevo estándar que permite hacer llamadas HTTP. La gran ventaja, frente a otros paquetes, es que devuelve promesas y por lo tanto es muy adecuado para utilizarlo con junto con async / await.

Si lo vamos a utilizar desde Node tenemos que instalar el paquete node-fetch, que nos ofrece una implementación bastante completa del estándar:

npm install node-fetch --save-dev

A partir de aquí sólo tenemos que invocar a fetch() recordando que como primer parámetro recibe la URL a la que queremos acceder y como segundo parámetro un objeto con las opciones que se deben utilizar.

Aquí tenemos ver un ejemplo:

const expect = require('chai').expect;
const fetch  = require('node-fetch');

describe('Fetch', function () {
  it('Debe devolver un objeto JSON', async () => {
    var response = await fetch('http://jsonplaceholder.typicode.com/users/1');
    expect(response.status).to.be.equal(200);
    var user = await response.json();
    expect(user).to.be.an('Object');
  });
});

Nota: en este ejemplo hemos utilizado una arrow function o función flecha: (done) =>. Simplemente hace algo más compacto nuestro código, pero no tiene utilidad práctica.

Para conocer más sobre fetch() podemos consultar https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API.

Async / Await

Para controlar la ejecución asíncrona en ES7 podemos utilizar async / await, un par de poderosas instrucciones del lenguaje que nos ofrecen una alternativa interesante al uso de callbacks.

Mocha está preparado para trabajar con promesas y una función declarada como async siempre devuelve una promesa, por lo que la integración con Mocha será muy sencilla y natural.

Vamos a ver un sencillo ejemplo:

const expect = require('chai').expect;

describe('Promise', function () {
  describe('resolve()', function () {
    it('Debe devolver el valor pasado como parámetro', async function () {
        var result = await Promise.resolve('ok');
        expect(result).to.be.equal('ok');
      });
    });
  });
});

El soporte de async / await todavía no es universal, ya que es parte de la especificación de ES7. En Node está disponible desde la 7.6. En los navegadores está bastante soportado en los más modernos.

Pruebas del API REST

Ya hemos introducido todos los elementos que vamos a utilizar y los hemos instalado en nuestro equipo, por lo que ya podemos centrarnos en desarrollar un conjunto de tests. Aquí sólo vamos a recoger algunos ejemplos básicos, pero como se podrá comprobar es sencillo ampliar estos tests con una mayor cobertura y complejidad.

Para los ejemplos vamos a utilizar los servicios de http://jsonplaceholder.typicode.com/ que nos ofrece un API REST de pruebas bastante sencilla. No tiene una utilidad real, no almacena los nuevos registros que añadimos o modificamos, pero permite construir ejemplos con rapidez.

Obtención de una matriz

Primer caso de prueba vamos a obtener todos los usuarios dados de alta. Es un caso muy habitual, obtener todos los recursos en una matriz y recorrer sus elementos comprobando que todos ellos son correctos.

const expect  = require('chai').expect;
const fetch   = require('node-fetch');

const SERVER = 'http://jsonplaceholder.typicode.com';

describe('API REST', function () {
  it('GET /users debe devolver todos los usuarios', async () => {

    const response = await fetch(SERVER + '/users');
    expect(response.status).to.be.equal(200);

    const users = await response.json();
    expect(users).to.be.an('Array');
    for (let usr of users) {
      expect(usr).to.be.an('Object');
      expect(usr.id).to.be.a('Number');
      expect(usr.name).to.be.a('String');
      expect(usr.username).to.be.a('String');
      expect(usr.email).to.be.a('String');
    }

  });
});

Encadenar varias llamadas al API

Ahora vamos a realizar varias llamadas una detrás de otra: primero consultaremos un usuario, obtendremos su id y consultaremos todos los posts de ese usuario. Para ello añadiremos este nueva llamada a it() dentro del describe() del ejemplo anterior, de esta forma vamos añadiendo casos de prueba a nuestro test.

const expect  = require('chai').expect;
const fetch   = require('node-fetch');

const SERVER = 'http://jsonplaceholder.typicode.com';

it('GET /users/%id%/posts debe devolver todos los post del usuario', async () => {

  const responseUsers = await fetch(SERVER + '/users?email=Rey.Padberg@karina.biz');
  expect(responseUsers.status).to.be.equal(200);

  const users = await responseUsers.json();
  expect(users).to.be.an('Array');
  expect(users.length).to.be.equal(1);

  const responsePosts = await fetch(SERVER + '/users/' + users[0].id + '/posts');
  expect(responsePosts.status).to.be.equal(200);

  const posts = await responsePosts.json();
  expect(posts).to.be.an('Array');
  for (let p of posts) {
    expect(p).to.be.an('Object');
    expect(p.userId).to.be.equal(users[0].id);
    expect(p.id).to.be.a('Number');
    expect(p.title).to.be.a('String');
    expect(p.body).to.be.a('String');
  }

});

Método POST

No todo van a set métodos GET, vamos a ver un caso donde añadiremos un post con el método POST (parece un galimatías, pero el post se refiere a un artículo publicado por el usuario y el POST a un método HTTP; son la misma palabra, pero no tienen nada que ver). Para ello añadimos este it() a nuestro conjunto de tests:

const expect  = require('chai').expect;
const fetch   = require('node-fetch');

const SERVER = 'http://jsonplaceholder.typicode.com';

it('POST /users/%id%/posts debe añadir un nuevo post al usuario', async () => {

  const responseUsers = await fetch(SERVER + '/users?email=Rey.Padberg@karina.biz');
  expect(responseUsers.status).to.be.equal(200);

  const users = await responseUsers.json();
  expect(users).to.be.an('Array');
  expect(users.length).to.be.equal(1);

  const responseNewPost = await fetch(SERVER + '/users/' + users[0].id + '/posts', {
    method: 'POST',
    Headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      "title": "Lorem Ipsum",
      "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
          "Etiam fringilla enim mi, a ornare augue sollicitudin id."
    })
  });
  expect(responseNewPost.status).to.be.equal(201);

  const post = await responseNewPost.json();
  expect(post).to.be.an('Object');
  expect(post.userId).to.be.equal(users[0].id);
  expect(post.id).to.be.a('Number');

});

Diferencia de un test con y sin async / await

Para mostrar la diferencia entre usar async / await con fetch() y usar algún otro paquete basado en callbacks como el popular request, vamos a actualizar un post del usuario. Como en los ejemplos anteriores tendremos que llamar varias veces al servidor de forma consecutiva.

En primer lugar vamos a ver el test con la función request():

const expect  = require('chai').expect;
const request = require('request');

const SERVER = 'http://jsonplaceholder.typicode.com';

describe('API REST', function () {
  it('PUT /posts/%id% - debe modificar un post (con request)', function (done) {

    request(SERVER + '/users?email=Rey.Padberg@karina.biz', {
        method: 'GET',
        json: true
      },
      function (err, responseUsers) {
        expect(err).to.be.equal(null);
        expect(responseUsers.statusCode).to.be.equal(200);
        expect(responseUsers.body).to.be.an('Array');
        expect(responseUsers.body.length).to.be.equal(1);

        request(SERVER + '/users/' + responseUsers.body[0].id + '/posts', {
            method: 'GET',
            json: true
          },
          function (err, responsePosts) {
            expect(err).to.be.equal(null);
            expect(responsePosts.statusCode).to.be.equal(200);
            expect(responsePosts.body).to.be.an('Array');

            request(SERVER + '/posts/' + responsePosts.body[0].id, {
                method: 'PUT',
                body: {
                  "title": "Lorem Ipsum",
                  "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
                      "Etiam fringilla enim mi, a ornare augue sollicitudin id."
                },
                json: true
              },
              function (err, responseEditPost) {
                expect(err).to.be.equal(null);
                expect(responseEditPost.statusCode).to.be.equal(200);
                expect(responseEditPost.body).to.be.an('Object');
                expect(responseEditPost.body.id).to.be.equal(responsePosts.body[0].id);
                done();
              }
            );
          }
        );
      }
    );
  });
});

Ahora vamos a ver el mismo test con async / await y fetch():

const expect = require('chai').expect;
const fetch  = require('node-fetch');

const SERVER = 'http://jsonplaceholder.typicode.com';

describe('API REST', function () {
  it('PUT /posts/%id% debe modificar un post', async () => {

    const responseUsers = await fetch(SERVER + '/users?email=Rey.Padberg@karina.biz');
    expect(responseUsers.status).to.be.equal(200);

    const users = await responseUsers.json();
    expect(users).to.be.an('Array');
    expect(users.length).to.be.equal(1);

    const responsePosts = await fetch(SERVER + '/users/' + users[0].id + '/posts');
    expect(responsePosts.status).to.be.equal(200);

    const posts = await responsePosts.json();
    expect(posts).to.be.an('Array');

    const responseEditPost = await fetch(SERVER + '/posts/' + posts[0].id, {
      method: 'PUT',
      Headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        "title": "Lorem Ipsum",
        "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +
        "Etiam fringilla enim mi, a ornare augue sollicitudin id."
      })
    });
    expect(responseEditPost.status).to.be.equal(200);

    const post = await responseEditPost.json();
    expect(post).to.be.an('Object');
    expect(post.id).to.be.equal(posts[0].id);

  });
});

Como se puede comprobar la versión que utiliza fetch con async / await es bastante más sencilla de leer que el modelo basado en request() con callbacks. Este es realmente un ejemplo sencillo, ya que en ocasiones tendremos que realizar pruebas mucho más complejas, realizando varias llamadas y finalmente borrando todos los elementos que hemos creado a fin de mantener las pruebas aisladas unas de otras. En esos casos más complejos la utilización de co() se vuelve aun más recomendable.

Conclusiones

En primer lugar debemos volver a insistir en las ventajas que nos ofrece fetch() al estar implementado de forma nativa en muchos navegadores y disponer de un paquete que nos permite utilizar este API en Node. De esta forma podemos hacer pruebas isomórficas que se ejecuten igual en el servidor y en el cliente. Además, el hecho de que devuelve promesas nos ayuda en la gestión de la asincronía de las llamadas al servidor.

Para el control de la ejecución asíncrona seguramente la mejor solución es utilizar async / await. Ofrece un mecanismo comprensible junto con las promesas de fetch(). Sin duda, nuestro código es más sencillo de leer y de mantener que con callback anidados.

Aquí sólo hemos mostrado unos ejemplos sencillos, pero creemos que se puede ver con claridad cómo es el mecanismo a utilizar para desarrollar este tipo de pruebas. Esperamos que sean de utilidad y sirvan de base para elaborar pruebas completas de las API REST.

Novedades

Native apps with Titanium por Rene Pot

Rene Pot nos cuenta cómo crear apps nativas con Titanium + Alloy y sacar el máximo partido en el desarrollo de aplicaciones nativas desde un único código fuente basado en Javascript.

10 patrones de diseño para Node por Felipe Polo

Los diez patrones de diseño para Javascript presentados por Felipe Polo en esta interesante charla te ayudarán a crear un código más legible, mantenible y comunicativo. Son un buen punto de partida para hacer mejor tus programas.

NPM Audit avisa de las vulnerabilidades en las dependencias

La aparición de npm audit es un importante hito en el ecosistema Javascript. Las vulnerabilidades de muchos paquetes llevan tiempo siendo conocidas, pero ahora se ponen de manifiesto de forma muy significativa. A medio plazo nos encontremos con un registro de paquetes con mejor salud, a corto plazo está produciendo una avalancha de apertura de incidencias.