Tests d'intégration en Node.js: de Supertest à Frisby.js
Nous avons exploré plusieurs outils qui facilitent l'écriture de tests d'intégration pour des APIs en Node.js. J'explique dans cet article pourquoi nous sommes passés de Supertest à Frisby.js.
TL;DR
Vous pouvez retrouver un exemple de mise en place de Frisby.js avec une API Node.js sur Github, marmelab/integration-test-rest-api.
Pourquoi faire des tests d'intégration ?
Pour la plupart des projets, vous devez écrire des tests si vous tenez à votre temps. En effet, il est préférable de trouver un bug à partir des tests que de recevoir un appel à 2 heures du matin et d’être amené à le corriger au plus vite. Cela peut paraître paradoxal, et pourtant, prendre le temps d’écrire des tests permet d’en gagner.
Write tests. Not too many. Mostly integration.
Un tweet de Guillermo Rauch que j'apprécie beaucoup.
Les tests d'intégration testent une fonctionnalité plus qu'on composant. Pour une API qui expose des données venant d'une base de données, un test d'intégration consiste à appeler une route d'API avec certains paramètres, et vérifier si le résultat correspond à la réponse attendue - sans mocker les couches basses.
Ce sont les tests qui m'apportent le plus de confiance dans un projet. Ils permettent de tester les lien entre l'ensemble des briques de l'application. Ils cassent pour les bonnes raisons : parce qu'une fonctionnalité est cassée, pas parce que l'implémentation a changé.
SuperTest
Sur certains projets, on utilise SuperTest, un outil pour tester les APIs écrites en Node.js qui répond bien à nos attentes. Il est rapide à mettre en place, simplifie l'écriture des tests et est utilisé par une grande communauté.
Voici un exemple de test:
import request from "supertest";
import express from "express";
// L'API à tester
const app = express();
app.get('/user', function(req, res) {
res.status(200).json({ name: 'john' });
});
// Un test typique avec supertest
describe('GET /user', function() {
it('responds with json', function(done) {
request(app)
.get('/user')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, done);
});
});
Sans entrer dans les détails de son fonctionnement, SuperTest lance une instance du serveur à chaque fois que request()
est appelé. Il n'y a donc pas à démarrer le serveur d'API pour lancer les tests. Supertest est très populaire et bien maintenu.
Cependant, comme nous l'avons expliqué dans l'article Mocking an API with Polly.js, le fait de ne pas connaître le port utilisé par l'API au moment des tests nous a posé des problèmes. Nous avons dû tordre Polly.js pour le faire fonctionner.
Pour moi, c'était le bon moment de remettre en cause l'utilisation de SuperTest. J'ai donc cherché un framework de test alternatif qui ne démarre pas l'API tout seul.
Frisby.js
Frisby.js correspond à notre besoin. Il combine un client HTTP avec une librairie d'assertion, le tout avec une API intuitive. C'est une surcouche à Jest qui rend les tests d'API plus faciles et rapides. Contrairement à Supertest, Frisby.js ne lance pas de serveur, il appelle notre API comme un utilisateur le ferait. On connaît le port utilisé pour les tests donc cela ne bloque pas l'utilisation de Polly.js.
J'ai créé un projet le plus simple possible pour montrer comment mettre en place Frisby.js, vous pouvez le retrouver sur marmelab/integration-test-rest-api.
Un exemple de test:
import frisby from "frisby";
describe('GET /user', function() {
it("should return valid users list", function () {
return frisby
.get("http://localhost:3000/users")
.expect("status", 200);
});
});
En plus de simplifier grandement l'écriture des tests, il permet de valider les différentes réponses en utilisant la librairie Joi.
Exemple de validation d'un objet:
import frisby from "frisby";
const Joi = frisby.Joi; // Frisby exposes Joi for convenience
const address = Joi.object({
street: Joi.string().required(),
suite: Joi.string().required(),
city: Joi.string().required(),
zipcode: Joi.string().required(),
geo: {
lat: Joi.string().required(),
lng: Joi.string().required(),
},
});
const company = Joi.object({
name: Joi.string(),
catchPhrase: Joi.string(),
bs: Joi.string(),
});
const user = Joi.object({
id: Joi.number().required(),
name: Joi.string().required(),
username: Joi.string().required(),
email: Joi.string().email(),
phone: Joi.string().required(),
website: Joi.string().uri({ allowRelative: true }).required(),
address,
company,
});
describe('GET /user', function() {
it("should return valid users list", function () {
return frisby
.get("http://localhost:3000/users")
.expect("status", 200)
.expect("jsonTypesStrict", "*", user);
});
});
Joi permet de décrire nos données en utilisant un langage simple, intuitif et lisible. Il nous simplifie grandement la validation des données et du schéma.
Notez que si Frisby.js a représenté un progrès pour nous en partant de Supertest, d'autres utilisateurs lui trouvent des défauts et ont choisi des alternatives.
Conclusion
Suite à cette exploration et découverte de Frisby.js, nous l'utilisons sur plusieurs projets. La partie validation nous offre un haut niveau de confiance sur le déploiement d'API.
Si votre API utilise une norme REST, l'utilisation de Joi n'est pas nécessaire. Si vous utilisez OpenAPI pour écrire un contrat d'API, vous pouvez le combiner à express-openapi-validator pour valider les réponses et les données automatiquement depuis votre contrat OpenAPI.
Je ne peux que vous recommander de vous lancer dans l'écriture de tests d'intégration pour votre API.