Testeo de API REST con Mocha y Chai-HTTP

A menudo, cuando desarrollamos una API, nos preguntamos qué podemos utilizar para hacer las pruebas. Este post va a explicar cómo llevar a cabo las pruebas de las principales peticiones HTTP (GET, POST, DELETE…) sobre una API node utilizando el framework de test MOCHA, la librería de aserciones CHAI y la librería que nos facilita las peticiones HTTP, Chai HTTP.

Aunque ya hablamos en el blog sobre cómo crear pruebas unitarias para nuestro desarrollo en JavaScript con Mocha y Chai, repasamos, de manera breve, qué es Mocha y Chai para ponernos en contexto.

MOCHA es un framework de pruebas para Node JS que puede ejecutarse desde la consola o desde un navegador. Como su propia web indica, permite realizar pruebas asíncronas de manera sencilla y divertida. Al ejecutar los test, permite la presentación de informes flexibles y precisos.

CHAI es una librería de aserciones BDD/TDD para Node JS y navegador, que puede ser armónicamente emparejado con cualquier framework Javascript.

Chai HTTP es una extensión de la librería CHAI, que permite realizar pruebas de integración con llamadas HTTP utilizando las aserciones de CHAI y todos los métodos de HTTP: GET, POST, PUT, DELETE, PATCH…

Para entender mejor cómo funciona Chai HTTP, se ha subido al GitHub de Paradigma un proyecto escrito en Node JS que contiene una API REST y las pruebas implementadas con Mocha y Chai HTTP.

La API generada permite gestionar un listado de países, cada uno de ellos  se compone de un ID, un nombre, un año y número de días. En el proyecto dentro de la carpeta test, disponemos del fichero testChaiHTTP.js que contiene ejemplos de POST, GET, PUT y DELETE. Veamos cada uno de ellos.

POST

En el fichero testChaiHTTP.js disponemos de dos test que realizan un post a nuestra API.

Primer ejemplo:

Primero requerimos los paquetes necesarios:

let chai = require('chai');
let chaiHttp = require('chai-http');
const expect = require('chai').expect;

Una vez que tenemos los paquetes requeridos, tenemos que decirle a Chai que utilice la librería de Chai HTTP y definimos la url donde vamos a lanzar las llamadas a la API.

chai.use(chaiHttp);
const url= 'http://localhost:3000';

Para montar el test con Mocha, primero encapsulamos el test dentro de la función describe, donde vamos a introducir una descripción del test que se va a realizar.

Dentro de dicha función llamamos a la función it, que es donde vamos a explicar lo que queremos que haga el test.

describe('Insert a country: ',()=>{

	it('should insert a country', (done) => {
		chai.request(url)
			.post('/country')
			.send({id:0, country: "Croacia", year: 2017, days: 10})
			.end( function(err,res){
				console.log(res.body)
				expect(res).to.have.status(200);
				done();
			});
	});
});

Nuestro test realiza un post sobre la API de países que hemos creado. Para insertar un nuevo país a la lista de países, se realiza un chai.request a la url que hemos definido, utilizando el método post de Chai HTTP, mandando el nuevo país en formato JSON.

Por último chequeamos con el método end, en cuya respuesta nos está devolviendo un mensaje en el body y un código 200.

Si ejecutamos el test obtenemos el siguiente resultado:

Segundo ejemplo:

En el segundo ejemplo vamos a realizar una llamada errónea a la API intentando introducir un país que no es un país, por lo que el API contestará con un error 500 en este caso.

describe('Insert a country with error: ',()=>{

	it('should receive an error', (done) => {
		chai.request(url)
			.post('/country')
			.send({id:1, country: "Madrid", year: 2010, days: 10})
			.end( function(err,res){
				console.log(res.body)
				expect(res).to.have.status(500);
				done();
			});
	});

});

Si ejecutamos el test obtenemos el siguiente resultado:

En la segunda línea podemos ver el mensaje de error que ha encapsulado el API.

GET

Tercer ejemplo:

Para este ejemplo vamos a obtener todos los países que tenemos introducidos en la lista de países. Para ello vamos a utilizar la función get.

describe('get all countries: ',()=>{

	it('should get all countries', (done) => {
		chai.request(url)
			.get('/countries')
			.end( function(err,res){
				console.log(res.body)
				expect(res).to.have.status(200);
				done();
			});
	});

});

Si ejecutamos el test obtenemos el siguiente resultado:

La API nos devuelve el listado con todos los países introducidos.

Cuarto ejemplo:

En este ejemplo vamos a utilizar también la función get, pero en este caso vamos a obtener un elemento concreto de la lista.

describe('get the country with id 1: ',()=>{

	it('should get the country with id 1', (done) => {
		chai.request(url)
			.get('/country/1')
			.end( function(err,res){
				console.log(res.body)
				expect(res.body).to.have.property('id').to.be.equal(1);
				expect(res).to.have.status(200);
				done();
			});
	});

});

Si ejecutamos el test obtenemos el siguiente resultado:

La API nos devuelve el país con ID 1.

PUT

Quinto ejemplo:

En este ejemplo se va a realizar una llamada a la función PUT para actualizar el valor de los días de un país, pasándole en la URL el ID del país y los días a aumentar.

describe('update the days of country with id 1: ',()=>{

	it('should update the number of days', (done) => {
		chai.request(url)
			.put('/country/1/days/20')
			.end( function(err,res){
				console.log(res.body)
				expect(res.body).to.have.property('days').to.be.equal(20);
				expect(res).to.have.status(200);
				done();
			});
	});

});

Si ejecutamos el test obtenemos el siguiente resultado:

Los días de Croacia han aumentado de 10 a 20.

DELETE

Sexto ejemplo:

En este ejemplo, se va eliminar un país de la lista. Primero vamos a obtener todos los países para comprobar que hay países en la lista, después vamos a eliminar el país con ID 1 utilizando la función del. Por último, vamos a obtener otra vez todos los países para comprobar que el país con ID 1 se ha eliminado.

describe('delete the country with id 1: ',()=>{

	it('should delete the country with id 1', (done) => {
		chai.request(url)
			.get('/countries')
			.end( function(err,res){
				console.log(res.body)
				expect(res.body).to.have.lengthOf(2);
				expect(res).to.have.status(200);
				chai.request(url)
					.del('/country/1')
					.end( function(err,res){
						console.log(res.body)
						expect(res).to.have.status(200);
						chai.request(url)
							.get('/countries')
							.end( function(err,res){
								console.log(res.body)
								expect(res.body).to.have.lengthOf(1);
								expect(res.body[0]).to.have.property('id').to.be.equal(0);
								expect(res).to.have.status(200);
								done();
						});
					});
			});
	});

});

Si ejecutamos el test obtenemos el siguiente resultado:

Al principio, tenemos los países con ID 0 e id 1 y después de eliminar el país con ID 1, revisamos la lista de países y comprobamos que el país con ID 1 no existe.

FORM

Séptimo Ejemplo

En el séptimo ejemplo vamos a ver cómo se enviaría mediante un POST, datos en forma de formulario. Para ello utilizamos type, que va a indicar que vamos a mandar los datos como un formulario.

describe('Insert a country with a form: ',()=>{

	it('should receive an error because we send the country in form format', (done) => {
		chai.request(url)
			.post('/country')
			.type('form')
			.send({id:0, country: "Croacia", year: 2017, days: 10})
			.end( function(err,res){
				console.log(res.body)
				expect(res).to.have.status(500);
				done();
			});
	});
});

Como resultado del test obtenemos el siguiente resultado:

El resultado que obtenemos es un error ya que no estamos dando los datos en el formato esperado, un JSON. Para comprobar el formato en el que se han mandado los datos, hemos puesto una traza en el servicio que expone la API para que se pueda observar el formato enviado.

Datos enviados como formulario.

AGENT and COOKIES

Octavo ejemplo

En este ejemplo se va a explicar el uso de un agent para realizar una autenticación en el sistema y a su vez, utilizar la cookie que nos proporciona el sistema de autenticación para hacer otra llamada.

Para ello primero declaramos el agent pasándole la url de la API:

var agent = chai.request.agent(url)

Después realizamos una autenticación básica en el sistema y obtenemos la cookie authToken. Una vez obtenida la cookie, gracias al agent que hemos creado no tenemos que volver a enviarla, el la almacena y en la siguiente llamada proporciona el token de autenticación que nos proporcionó el sistema:

describe('Authenticate a user: ',()=>{

	it('should receive an OK and a cookie with the authentication token', (done) => {
		agent
			.get('/authentication')
  			.auth('user', 'password')
			.end( function(err,res){
				console.log(res.body)
				expect(res).to.have.cookie('authToken');
				expect(res).to.have.status(200);
				return agent.get('/personalData/user')
      					.then(function (res) {
         						expect(res).to.have.status(200);
         						console.log(res.body)
         						done();
      					});
			done();
		});
	});

});

Al ejecutar el test obtenemos el siguiente resultado:

Como podemos observar en la ejecución del test obtenemos el OK del servidor que nos indica que el usuario se encuentra en la BBDD y nos proporciona el token de autenticación a través de la cookie. Realizamos la segunda llamada para obtener los datos personales del usuario y obtenemos un JSON con los datos.

El agent almacena la cookie sólo para un test. Para comprobar este comportamiento se ha realizado un test que comprueba que si hacemos la misma llamada para obtener los datos personales del usuario, sin realizar antes la autenticación, nos devuelve un error 500 el servidor.

describe('Obtain personal data without authToken: ',()=>{

	it('should receive an error because we need authToken', (done) => {
		agent
			.get('/personalData/user')
      			.then(function (res) {
         				expect(res).to.have.status(500);
         				console.log(res.body)
      		});
		done();
	});

});

El resultado de la ejecución del test es el siguiente:

El servidor retorna un código 500 informando de que se ha producido un error al intentar acceder a los datos sin estar autenticado.

Otras Funcionalidades

Chai HTTP es una herramienta de testeo de APIs bastante robusta, además de los ejemplos básicos que se han realizado, permite encapsular en las peticiones bastantes más objetos como por ejemplo:

  • Enviar una cabecera:
.set('X-API-Key', 'foobar')
  • Enviar los datos como un formulario:
.type('form')
	  .send({
	    '_method': 'put',
	    'password': '123',
	    'confirmPassword': '123'
	  }
  • Adjuntar parámetros de consulta:
.query({name: 'foo', limit: 10})
  • Adjuntar ficheros:
.attach('imageField', fs.readFileSync('avatar.png'), 'avatar.png')
  • Chequear si hemos recibido alguna cookie:
expect(res).to.have.cookie('sessionid');

Estos son algunos ejemplos de funcionalidades que Chai HTTP permite. Para más información, puedes consultar su GitHub.

Exclusivas de Chai HTTP

Chai HTTP también cuenta con una serie de aserciones exclusivas que se pueden combinar con cualquiera de las que ya incluye Chai. Algunas ya las hemos visto cómo status que comprueba el código de estado de la petición o la aserción cookie que comprueba si dispone de una cookie. Pero hay muchas más como por ejemplo:

  • HeaderQue comprueba las cabeceras que se han recibido en la respuesta.
        expect(req).to.have.header('x-api-key');
  • JSON o HTML o TEST permiten comprobar el content type de la respuesta recibida.
        expect(req).to.be.json;
	expect(req).to.be.html;
	expect(req).to.be.text;
  • RedirectTo: permite redireccionar una respuesta a una url destino especificada.
	expect(res).to.redirectTo('http://example.com');

Estos son algunos ejemplos, para más información consultar su GitHub.

Ejecución

Para poder lanzar los test o correr el API necesitamos instalar Node Js.

Una vez instalado Node JS y descargado el proyecto, nos situamos en la carpeta del proyecto y ejecutamos:

npm install

Una vez instaladas las dependencias, ejecutamos el servidor que contiene nuestra API escuchando en el puerto 3000, con el comando:

node server.js

Para lanzar los test realizados se tiene que introducir el comando:

mocha test/*.js --timeout 15000

Conclusión

Finalmente la elección de la librería de pruebas se elige en función del lenguaje de programación con el que se desarrolla la API, para que todo esté unificado bajo un mismo lenguaje de programación.

Por lo que si la API está escrita en NodeJS, personalmente recomiendo utilizar Chai HTTP con Mocha y Chai porque son herramientas compatibles, fáciles de utilizar y rápidas.

Se pueden encontrar bastantes casos de ejemplo y dudas resueltas en Internet lo que hace que sea una herramienta utilizada por la comunidad.

Pero no sólo de Chai HTTP vive el hombre, hay muchas alternativas para realizar pruebas sobre APIs por lo que invito a utilizar y experimentar todas y adaptar la que más guste al equipo.

Ingeniero informático desde 2012. Pegándome con bugs desde 2013 (ง'̀-'́)ง Miembro del equipo de QAradigma. Gran aficionado a los videojuegos desde que tengo memoria y apasionado de los ordenadores desde que a mi hermano le regalaron un 486.

Ver toda la actividad de Nicolás Cordero

2 comentarios

  1. rethPach dice:

    Hola, el post esta muy bien, aunque aqui los ejemplos estas testeando chai, la idea seria como testear un api usando estas herramientas, mi opinion muy personal: solo agregan complejidad ya que tdd realmente va de codigo facil de testear, Saludos.

    • Nicolás Cordero dice:

      Hola, muchas gracias por tu comentario. En este post queríamos presentar Chai HTTP para realizar pruebas de integración sobre una API muy básica que he implementado y mostrar las funcionalidades y servicios que ofrece Chai HTTP para realizar este tipo de pruebas. De cara a proyectos con APIs mas complejas estas herramientas pueden ser de gran ayuda al problema que bien nos comentas, ya que para realizar un TDD con éxito las pruebas deben ser lo mas sencillas y mantenibles posible, independientemente de la cantidad o complejidad del código a testear.

Escribe un comentario