Testing de aplicaciones Express con Mocha y Supertest

Uso básico de Mocha para testing Javascript

Para instalar Mocha, en el directorio de nuestro proyecto (y suponiendo que ya tenemos creado un package.json) hacemos

npm install -g --save-dev mocha

-g hace que la herramienta CLI mocha se instale globalmente para poder usarse en otros proyectos. De lo contrario se instalaría dentro de node_modules. Con --save-dev añadimos la dependencia al proyecto pero en modo desarrollo.

Si en un proyecto Node bajado de la web hacemos npm install para instalar las dependencias, se instalarán todas (también las de desarrollo), pero si hacemos npm instal --production las de desarrollo no se instalarán.

Testing de código síncrono

Por ejemplo, supongamos que tenemos el siguiente módulo, sobre el que vamos a hacer unas cuantas pruebas:

var obj = {
    saludar: function() {
        return "hola mundo";
    }
};
module.exports = obj;

El fichero con la suite de pruebas tendría un formato como el siguiente:

var s = require('saludador');
var assert = require('assert');

describe('mi suite', function(){
    it ('saludar() debería ser una función', function(){
        //comprueba que es true, o sea no es undefined
        assert(s.saludar);  
        //comprueba que es una función
        assert.equal("function", typeof(s.saludar));
    });

    it('saludar() debería devolver hola mundo', function(){
        assert.equal("hola mundo",s.saludar());
    });
})

Testing de código asíncrono

Como ya hemos visto en muchos ejemplos, gran parte del código Javascript del "mundo real" es asíncrono. Por ejemplo el siguiente código, que hace una petición HTTP para obtener la página principal de la UA:

//para simplificar el código usamos el paquete 'request'
//npm install --save request
request('http://www.ua.es', function(error, respuesta, body) {
    console.log(body);
});

Supongamos que queremos comprobar que la página de la UA contiene la cadena "Universidad de Alicante". En una primera instancia, podríamos hacer algo como:

describe('mi suite asíncrona', function(){
    it ('la petición a www.ua.es contiene el nombre de la UA', function(){
        request('http://www.ua.es', function(error, respuesta, body) {
            //indexOf devuelve la posición de una subcadena dentro de otra
            //si la subcadena no está devuelve -1
            assert(body.indexOf("Universidad de Alicante")!=-1);
        });
    });
})

Aunque el test anterior va a pasar, en realidad es porque no se está comprobando nada. Mocha ejecuta la suite, y antes de que llegue la respuesta de la UA (y se ejecute el callback con el assert) la suite ya ha terminado de ejecutarse. El assert no llega nunca a ser comprobado.

Puedes verificar que lo anterior es cierto cambiando la cadena buscada por cualquier otra que no esté en la página. Verás que el test sigue pasando sin problemas

La solución es ejecutar una función que le indique a Mocha que ya han acabado nuestros tests. Hasta que no se ejecute esta función Mocha considerará que el test no ha acabado y por tanto "esperará". Dicha función nos la pasa Mocha en el callback asociado a cada test, y en la documentación y ejemplos se suele llamar done:

describe('mi suite asíncrona', function(){
    it ('la petición a www.ua.es contiene el nombre de la UA', function(done){
        request('http://www.ua.es', function(error, respuesta, body) {
            //seguro que este assert se ejecuta, todavía no hemos hecho done()
            assert(body.indexOf("Universidad de Alicante")>=0);
            //ya ha acabado el test
            done();
        });
    });
})

Uso de supertest

Para hacer pruebas de APIs REST nos viebe bien poder realizar peticiones de modo sencillo. Para esto podemos ayudarnos del paquete supertest. Lo instalaremos, como viene siendo habitual con:

npm install --save-dev supertest

Por ejemplo supongamos que tenemos el siguiente código Express que queremos probar:

var express = require('express');
var app = express();

//En Express asociamos un método HTTP y una URL con un callback a ejecutar
app.get('/', function(pet,resp) {
   //Tenemos una serie de primitivas para devolver la respuesta HTTP
   resp.status(200);
   resp.set('X-Mi-Cabecera', 'Hola')
   resp.send('Hola soy Express'); 
});

//Este método delega en el server.listen "nativo" de Node
app.listen(3000, function () {
   console.log("El servidor express está en el puerto 3000");
});

module.exports = app;

La mejor forma de ver el uso de supertest es a través de un ejemplo:

var hola_express = require('hola_express');
var supertest = require('supertest');


describe('prueba de la app web', function(){
    it('/ devuelve el contenido adecuado', function(done){
        //Al objeto supertest le pasamos la app de Express
        supertest(hola_express)
            //Hacemos una petición HTTP
            .get('/')
            //Supertest incluye sus propias aserciones con 'expect'
            //Cuando ponemos un entero estamos verificando el status HTTP
            .expect(200)
            //Cuando ponemos dos String estamos verificando una cabecera HTTP
            .expect('X-Mi-Cabecera', 'hola')
            //Si ponemos un string  estamos verificando el cuerpo de la respuesta
            //Como esta ya es la última expectativa, pasamos el 'done'. Supertest lo llamará
            //Cualquier 'expect' admite el 'done' como último parámetro
            .expect('Hola soy Express', done);
    });
    it('La ruta /hola no existe', function(done){
        supertest(hola_express)
            .get('/hola')
            .expect(404, done);
    });
});

Supertest usa internamente la librería superagent, así que podemos consultar en la documentación de esta última cómo realizar otro tipo de peticiones como POST o PUT

Si las formas de expect que ya hemos visto no se adaptan a nuestras necesidades, siempre podemos pasar una función en la que pondremos los assert que queramos. Dicha función recibirá automáticamente como parámetro un objeto response de superagent.

supertest(hola_express)
    .get('/')
    .expect(function(respuesta){
        //En la referencia del objeto response de superagent 
        //podemos ver la propiedad 'text': el texto "raw" del cuerpo de la respuesta
        assert(respuesta.text.indexOf('Express')!=-1);
    })
    //El callback de antes no recibe el 'done', así que tenemos que usar 'end',
    //que acaba el test llamando a la función que le pasemos
    .end(done);