Testeando un API REST node.js con Mocha

Los test son una parte fundamental del desarrollo de software. Hay diferentes prácticas como TDD, BDD y aparte diferentes tipos de test como test de aceptación, test de seguridad etc..
Indistintamente de las prácticas, nombres y demás, cuando desarrollamos una API queremos que se comporte como debe cuando se realizan peticiones. Por ejemplo si realizo una petición a un endpoint que no existe debería devolverme un 404 como código de respuesta, si hago un post para crear un recurso debe devolverme un 201 y una cabecera location con la url donde se puede acceder al recurso creado.
En este artículo vamos a ver cómo realizar test end to end a un API REST escrita en Node.js utilizando Mocha y SuperTest. El objetivo del artículo no es entrar en si se debe escribir estos tests antes o después que el código de producción o si se debe crear antes o después que los test unitarios de la lógica de negocio o dominio, el objetivo es ver como escribir esos test utilizando mocha y supertest, indistintamente de cuando se haga.

Introducción a Mocha

Mocha es un framework de test para Node.js, los test pueden ser ejecutados desde el terminal o el navegador. También existen IDEs y editores de texto que tienen integración con Mocha y desde la propia herramienta, donde escribimos el código, podemos ejecutar los test y ver los resultados.
Mocha en Node.js tiene la misma finalidad que JUnit en Java o xUnit en .Net. La diferencia es que en mocha realizar test de código asíncrono es bastante más sencillo, esto es básico en una tecnología basada en la asincronía.

Interfaces

El sistema de interfaces de mocha permite elegir el estilo de DSL que queremos utilizar en los test. Mocha tiene BDD, TDD, Exports, QUnit y Require-style.

Para los ejemplos voy a utilizar BDD como DSL.
En Mocha para BBD tenemos los siguientes conceptos:

  • describe(), se utiliza para agrupar los tests o suite de tests.
  • context(), es un alias para describe, solo provee otra forma de agrupación de tests .
  • it(), representa un test.
  • before(), la función dentro de before se va a ejecutar antes del primer test dentro del describe o context.
  • after(), la función dentro de after se va a ejecutar después del último test dentro del describe o context.
  • beforeEach(), la función dentro de beforeEach se va a ejecutar antes de cada test dentro del describe o context.
  • afterEach(), la función dentro de afterEach se va a ejecutar después de cada test dentro del describe o context.
describe('Array', function() {  
    before(function() {
      // ...
    });

    describe('#indexOf()', function() {
      context('when not present', function() {
        it('should not throw an error', function() {
          (function() {
            [1,2,3].indexOf(4);
          }).should.not.throw();
        });
        it('should return -1', function() {
          [1,2,3].indexOf(4).should.equal(-1);
        });
      });
      context('when present', function() {
        it('should return the index where the element first appears in the array', function() {
          [1,2,3].indexOf(3).should.equal(2);
        });
      });
    });
  });

Introducción a supertest

Supertest es un módulo de Node.js con interface Fluent API que nos provee un nivel alto de abstacción para probar servicios http.
Por debajo utiliza superagent como cliente http.

Lo que aporta supertest son las aserciones vía Fluent API.
Como aserciones tenemos diferentes sobrecargas de la función expect:

  • expect(status[, fn]), verifica el código de estado de la respuesta.

  • expect(status, body[, fn]), verifica el código de estado de la respuesta y el body.

  • expect(body[, fn]), verifica el body con un string, expresión regular o también un objeto parseado.

  • expect(field, value[, fn]), verifica una cabecera con un string o expresión regular.

  • expect(function(res) {}), función de verificación personalizada, recibe el objeto de respuesta. Si la respuesta esta ok debería devolver false o no devolver nada. Si la vericación falla debemos devolver un error o un string especificando porque ha fallado la verificación.

Testeando un API REST

Los test que vamos a realizar son sobre Catalog-API, utilizado como ejemplo en el artículo Primeros pasos para crear un API REST con Node.js.

Instalando Mocha

Necesitamos instalar mocha a nivel global. Para hacerlo utilizamos el Node Package Manager desde el terminal:

$ npm install -g mocha

Creando carpeta para los tests

Creamos una carpeta para los tests. Si estamos manejando un IDE como Web Storm, deberemos editar la configuración de ejecución de test para indicarle donde esta el motor de mocha, donde estan los test etc..

Creando tests contra el API

Para crear los tests tenemos que hacer require de assert, por si hacemos aserciones manuales, supertest para realizar peticiones al API y app.js, donde iniciamos el API REST.

  • Responde por defecto en Json
    Creamos un test que verifica que al hacer una petición Get al endpoint /api/products, por defecto debe responder el body en formato Json.
"use strict"

 var assert = require('assert');
 var request = require('supertest')
 var app = require('../app.js')

 var request = request("http://localhost:8080")

 describe('products', function() {
     describe('GET', function(){
         it('Should return json as default data format', function(done){
             request.get('/api/products')
                 .expect('Content-Type', /json/)
                 .expect(200, done);
         });
     });
 });
  • Responde Json si se especifica en la cabecera Accept
    Añadimos un test que verifica que si se hace una petición Get al endpoint /api/products, con la cabecera Accept como application/json debe responder el body en formato Json.
"use strict"

 var assert = require('assert');
 var request = require('supertest')
 var app = require('../app.js')

 var request = request("http://localhost:8080")

 describe('products', function() {
     describe('GET', function(){
         it('Should return json as default data format', function(done){
             request.get('/api/products')
                 .expect('Content-Type', /json/)
                 .expect(200, done);
         });
         it('Should return json as data format when set Accept header to application/json', function(done){
             request.get('/api/products')
                 .set('Accept', 'application/json')
                 .expect('Content-Type', /json/)
                 .expect(200, done);
         });
     });
 });
  • Creamos un recurso
    Añadimos otro test que verifica que si se crea un producto, devuelve 201 como código de respuesta y una cabecera location para navegar al recurso creado.
"use strict"

 var assert = require('assert');
 var request = require('supertest')
 var app = require('../app.js')

 var request = request("http://localhost:8080")

 describe('products', function() {
     describe('GET', function(){
         it('Should return json as default data format', function(done){
             request.get('/api/products')
                 .expect('Content-Type', /json/)
                 .expect(200, done);
         });
         it('Should return json as data format when set Accept header to application/json', function(done){
             request.get('/api/products')
                 .set('Accept', 'application/json')
                 .expect('Content-Type', /json/)
                 .expect(200, done);
         });
     });
     describe('POST', function(){
         it('Should return 201 status code and location header', function(done){

             let product = {sku: "ab48cicj36734",
                            asin: "B015E8UTIU",
                            upc: "888462500449",
                            title: "Apple iPhone 6s 64 GB US Warranty Unlocked Cellphone - Retail Packaging (Rose Gold)",
                            image: "http://ecx.images-amazon.com/images/I/91DpCeCgSBL._SL1500_.jpg"}

             request.post('/api/products')
                 .send(product)
                 .expect(201)
                 .expect('Location', '/api/products/ab48cicj36734',done);
         });
     });
 });

Ejecutando los tests

Para ejecutar los test podemos hacerlos desde un IDE como WesStorm, configurando previamente, como hemos dicho, la ejecución de test. Aquí vamos a ver como ejecutarlos desde el terminal.

Nos posicionamos en la carpeta del proyecto desde el terminal y ejecutamos mediante el comando mocha:

$ mocha


restify listening at http://[::]:8080  
  products
    GET
      ✓ Should return json as default data format
      ✓ Should return json as data format when set accept header to json
    POST
      ✓ Should return 201 status code and location header


  3 passing (38ms)

Es importante que la carpeta donde tenemos los test se llame test en singular, de lo contrario nos dará error:

$ mocha
/usr/local/lib/node_modules/mocha/lib/utils.js:626
        throw new Error("cannot resolve path (or pattern) '" + path + "'");
        ^

Error: cannot resolve path (or pattern) 'test'  
    at Object.lookupFiles (/usr/local/lib/node_modules/mocha/lib/utils.js:626:15)
    at /usr/local/lib/node_modules/mocha/bin/_mocha:316:30
    at Array.forEach (native)
    at Object.<anonymous> (/usr/local/lib/node_modules/mocha/bin/_mocha:315:6)
    at Module._compile (module.js:435:26)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:311:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:134:18)
    at node.js:961:3

Conclusiones

Es fundamental tener bien testeada un API REST, para conseguirlo mocha como framework de test y supertest como librería para invocar y verificar nuestros endpoints son una gran combinación.

Todo el código utilizado está subido a GitHub, os animo a descargarlo, probarlo.