Tests fonctionnels d'une API REST en NodeJS

Nous testons tous nos applications. Autrement, comment pourrions nous savoir si ce que nous venons de coder fonctionne ?

Mais il nous faut des tests automatisés, car on ne peut pas se fier qu’aux tests manuels, ils ne sont pas toujours éxecutés de la même façon, avec la même rigueur.

Les tests fonctionnels

Dans cet article, je ne vais présenter que les tests fonctionnels, permettant de vérifier le bon comportement d’une fonctionnalité. Ils permettent également de documenter le projet, une documentation qui sera toujours à jour, sinon les tests ne passent plus.

Les tests fonctionnels sont la pour vérifier que le code répond bien aux spécifications du produit.

Ces tests testent notre application comme une “boîte noire”. Techniquement ils pourraient être développés dans un autre langage que le code à tester.

Prérequis

Avant de pouvoir commencer, il vous faudra installer :

Mise en place du code à tester

Vous allez tester le code développé dans cet article, disponible sur github.

Commencez par cloner le dépot

git clone https://github.com/ZwoRmi/todoListApi.git

Puis installez les dépendances du projet

npm install

Je ne vais pas le détailler dans cet article, mais pour que ces tests fonctionnels ne détruisent pas votre base de données de dev/prod, vous pouvez regarder le module config afin de spécifier une base de données différente pour chaque environnement.

Tests

Dépendances

Il vous faut tout d’abord installer les librairies vous permettant de tester cette API

npm install mocha chai chai-http --save-dev

Mocha est une librairie nous fournissant un environnement pour tester notre application de manière asynchrone. Il n’impose pas de librairie d’assertion, dans ce tutoriel, on utilisera chai. En plus de chai, nous allons utiliser chai-http pour faire nos appels HTTP.

Ensuite il faut créer la structure de fichiers pour vos tests

mkdir test
touch test/todoList.js

Tests de la route GET

C’est parti pour les premiers tests, à écrire dans le fichier que l’on vient de créer.

const mongoose = require('mongoose');
const chai = require('chai');
const should = chai.should();
const chaiHttp = require('chai-http');

const server = require('../index');
const todoItem = require('../api/model/TodoItem');
const TodoItem = mongoose.model('TodoItem', todoItem);

chai.use(chaiHttp);

describe('TodoList', () => {
beforeEach((done) => {
TodoItem.remove({}, () => { // On vide la base de données avant chaque test
done(); // Etant donné que la méthode remove est asynchrone, done est utilisé pour que mocha sache quand tout est terminé
});
});

describe('/GET todoitems', () => { // La suite de tests pour la route GET
it('should get all todo items when no items are in database', (done) => { // Test qui vérifie qu'il n'y a pas d'erreurs lorsque la base de données est vide
chai.request(server).get('/todoitems').end((err, res) => { // On requète la route GET
res.should.have.status(200); // On vérifie le statu de la réponse
res.body.should.be.a('array'); // On vérifie que le résultat est un tableau
res.body.length.should.be.eql(0); // On vérifie que la longueur du tableau est de 0
done(); // On dit à mocha que l'on a fini nos assertions
});
});

it('should get all todo items when there are two items in the database', (done) => { // Test qui vérifie qu'on a le bon résultat lorsqu'il y a deux items dans la base de données
const firstTodoItem = new TodoItem({
name: 'firstTask',
status: 'inProgress'
});
const secondTodoItem = new TodoItem({
name: 'secondTask',
status: 'done'
});
firstTodoItem.save(() => { // On sauvegarde les items dans la base de données
secondTodoItem.save(() => {
chai.request(server).get('/todoitems').end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('array');
res.body.length.should.be.eql(2);
res.body[0].name.should.be.eql(firstTodoItem.name); // On vérifie que les éléments retournés par la route sont semblables à ceux que l'on a enregistré dans la base
res.body[0].status.should.be.eql(firstTodoItem.status);
res.body[1].name.should.be.eql(secondTodoItem.name);
res.body[1].status.should.be.eql(secondTodoItem.status);
done();
});
});
});
});
});
});

Afin de lancer vos tests, vous allez devoir modifier le script test dans le fichier pachage.json en écrivant simplement mocha

{
"name": "my-first-node-api",
"version": "1.0.0",
"description": "My first NodeJs REST API to interact with a TodoList",
"main": "index.js",
"scripts": {
"test": "mocha",
"start": "node index.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"body-parser": "^1.17.1",
"express": "^4.15.2",
"mongoose": "^4.9.6"
}
}

Puis lancez ces deux tests

npm test

Le retour doit ressembler à ça :

> mocha

Your first node api is running on port: 3000


TodoList
/GET todoitems
√ should get all todo items when no items are in database (41ms)
√ should get all todo items when there are two items in the database


2 passing (110ms)

Tests de la route POST

Pour cette route il va y avoir trois tests :

  1. Un test qui vérifie qu’une erreur est retournée dans le cas ou le nom de la tâche n’est pas renseigné
  2. Un test qui vérifie que la valeur par défaut du statut de la tâche est todo
  3. Un test qui vérifie que la tâche est bien enregistré avec son statut

Ecrivez ces trois tests dans le même fichier que précédemment

describe('/POST todoitems', () => {
it('should not post a todo item when the task name is undefined', (done) => {
const param = {
status: 'inProgress'
};
chai.request(server).post('/todoitems').send(param).end((err, res) => {
res.should.have.status(500);
res.body.should.be.a('object');
done();
});
});

it('should post a todo item and the status default value should be todo', (done) => {
const param = {
name: 'test task name'
};
chai.request(server).post('/todoitems').send(param).end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('object');
res.body.should.have.property('name').be.eql(param.name);
res.body.should.have.property('status').be.eql('todo');
done();
});
});

it('should post a todo item with a status value and save it', (done) => {
const param = {
name: 'test task name',
status: 'done'
};
chai.request(server).post('/todoitems').send(param).end(() => {
TodoItem.find({}, (err, todoItems) => {
todoItems.length.should.be.eql(1);
todoItems[0].name.should.be.eql(param.name);
todoItems[0].status.should.be.eql(param.status);
done();
});
});
});
});

Tests de la route GET/:id

Cette fois-ci, deux tests s’imposent:

  1. Un test qui vérifie que le résultat est null lorsque l’id de la tâche recherchée n’existe pas
  2. Un test qui vérifie qu’une tâche est retournée lorsqu’on la recherche avec son id

Toujours dans le même fichier, écrivez ces deux nouveaux tests

describe('/GET/:id todoitems', () => {
it('should not get a todo item because it does not exist', (done) => {
chai.request(server).get('/todoitems/' + mongoose.Types.ObjectId()).end((err, res) => {
res.should.have.status(200);
should.not.exist(res.body);
done();
});
});

it('should get a todo item', (done) => {
const aTodoItem = new TodoItem({
name: 'firstTask',
status: 'inProgress'
});
aTodoItem.save((err, savedTodoItem) => {
chai.request(server).get('/todoitems/' + savedTodoItem._id).end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('object');
res.body.name.should.be.eql(aTodoItem.name);
res.body.status.should.be.eql(aTodoItem.status);
done();
});
});
});
});

Tests de la route PUT/:id

Pour cette route, les tests seront :

  1. Un test qui vérifie le résultat est null lorsque l’item à modifier n’existe pas
  2. Un test qui vérifie que le nom de la tâche puisse être modifié
  3. Un test qui vérifie que le statut puisse être modifiée

Ajoutez donc ces tests

describe('/PUT todoitems', () => {
it('should not put a todo item because it does not exist', (done) => {
const param = {
name: 'new name'
};
chai.request(server).put('/todoitems/' + mongoose.Types.ObjectId()).send(param).end((err, res) => {
res.should.have.status(200);
should.not.exist(res.body);
done();
});
});

it('should put a todo item and update the name value', (done) => {
const item = new TodoItem({
name: 'task name',
status: 'inProgress'
});
const param = {
name: 'new task name'
};
item.save(() => {
chai.request(server).put('/todoitems/' + item._id).send(param).end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('object');
TodoItem.find({}, (err, items) => {
items[0].name.should.eql(param.name);
items[0].status.should.eql(item.status);
done();
});
});
});
});

it('should put a todo item and update the status value', (done) => {
const item = new TodoItem({
name: 'task name',
status: 'inProgress'
});
const param = {
status: 'done'
};
item.save(() => {
chai.request(server).put('/todoitems/' + item._id).send(param).end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('object');
TodoItem.find({}, (err, items) => {
items[0].name.should.eql(item.name);
items[0].status.should.eql(param.status);
done();
});
});
});
});
});

Tests de la route DELETE/:id

Nos derniers tests concernent la route DELETE:

  1. Un test qui vérifie que le résultat est null lorsqu’on supprime un élément qui n’existe pas
  2. Un test qui vérifie qu’un item qui existe est bien supprimé
describe('/DELETE/:id todoitems', () => {
it('should not delete a todo item because it does not exist', (done) => {
chai.request(server).delete('/todoitems/' + mongoose.Types.ObjectId()).end((err, res) => {
res.should.have.status(200);
should.not.exist(res.body);
done();
});
});

it('should delete a todo item', (done) => {
const aTodoItem = new TodoItem({
name: 'firstTask',
status: 'inProgress'
});
aTodoItem.save((err, savedTodoItem) => {
chai.request(server).delete('/todoitems/' + savedTodoItem._id).end((err, res) => {
res.should.have.status(200);
res.body.should.be.a('object');
TodoItem.find({}, (err, res) => {
res.length.should.eql(0);
});
done();
});
});
});
});

Vous pouvez trouver le code sur le même repository que le code à tester, dans la branche testing.