Tests d'intégration
Les tests unitaires sont bien, mais il est assez rare que les modules de notre plateforme fonctionnent dans une vide.
La plupart du temps, il y a une interaction avec, au moins, une base de données.
Comment on pourrait tester notre code en sachant qu'il faudrait lancer une appli ou un module externe, même avant de tourner nos tests ?
Le cas de la base de données
La base de données présente un challenge pour nos tests automatiques.
On pourrait juste tester contre notre base de développement, mais il faut que les tests soient répétables. Si on modifie le schéma en dev, ou on ajoute des données supplémentaires, on risque de casser nos tests.
Idéalement, on utilise une base de données uniquement dédiée à nos tests :
Avant de lancer nos tests, on supprime l'ancienne base (si elle existe)
On recrée le schéma
On crée un utilisateur de test (qui aura les mêmes droits que notre API ou module en question)
On préremplit la base avec des données (qu'on appelle les données seed)
Cette procédure assure que nos tests restent répétables.
Il y a encore un avantage : dans l'esprit de CI/CD, cette procédure pourrait être répétée n'importe où (y compris sur notre serveur de développement)
Docker au secours
Docker est donc idéal pour des tests d'intégration. Avec un docker-compose.yml
bien écrit, on pourrait créer sans effort l'environnement de dépendances nécessaires pour nos tests (par exemple, lancer un MariaDB, Redis, ... ) puis lancer nos tests automatiques.
Pour notre environnement de développement, on a déjà une instance de MariaDB qui est créée. Super ! Quand on lance notre API en développement, on parle par défaut avec le service dbms
et la database qui s'appelle school
.
Nous n'avons qu'à ajouter une deuxième database, qu'on appellera school_test
. Nous allons interagir avec cette base via l'utilisateur api-test
.
Dans l'esprit de l'environnement de développement, nous allons créer la database et l'utilisateur dans un fichier dbms/ddl/init-test.sql
Jusqu'au présent, nous avons utilisé use school;
dans le DDL. Si on veut réutiliser le même DDL pour nos tests, on sera obligé d'enlever cette ligne du DDL.
Pour mettre à jour le schéma de la base de données, nous serions obligés de désormais préciser le nom de la database sur la ligne de commande mycli : mycli -h dbms -u root school < ./dbms/ddl/ddl.sql
Outil pour réinitialiser notre base de données
On aura besoin d'un outil qui permet de remettre à zéro notre base de données (récréer la base school_test
et recréer son schéma).
Cette opération est en dehors de l'utilisation de notre API de base, parce qu'il y aura des opérations normalement interdites :
drop database
create table
etc.
Pour nos tests uniquement, nous allons se connecter d'abord en tant que l'utilisateur root
, effectuer ces opérations, puis laisser l'utilisateur de l'API reprendre la main.
Pour cela, j'ai créé une classe utilitaire qui s'appelle test/utility/RootDB.ts
. Ce fichier est dans le dossier test
pour ne pas l'inclure lors de notre build en production.
Test d'intégration
Nous allons utiliser une librairie de plus, chai-as-promised
qui permet d'exprimer nos assertions qui concernent des Promises (des opérations async
).
On pourrait, par exemple, tester une opération CRUD pour l'ajout d'un utilisateur (test/integration/suites/User.integration.ts
)
Note bien l'utilisation du hook before
et after
. Ce sont les fonctions appelées avant tous les tests de ce fichier et après tous les tests. Cela permet d'initialiser la base de données, et aussi fermer la connexion à la fin de tous les tests.
Il faut donc ajouter la fonction Close()
à la classe src/utility/DB.ts
:
Lancer les test d'intégration
Il faut maintenant lancer nos tests. Par contre, on aura besoin de bien préciser les valeurs pour nos variables d'environnement. Souvenez qu'on utilise au moins :
DB_HOST
: normalementdbms
(selon notredocker-compose.yml
)DB_DATABASE
: le nom de la base à utiliser. Pour le dev, c'estschool
, mais pour nos tests, on va plutôt utiliserschool_test
DB_USER
: le nom d'utilisateurDB_PASSWORD
: le mot de passeDB_ROOT_USER
: le nom d'utilisateurroot
DB_ROOT_PASSWORD
: le mot de passe de l'utilisateurroot
On devrait donc fournir un .env
qui va fixer toutes ses variables uniquement pour nos tests.
Moi, j'ai créé un fichier test/.env.test
qui reprend tous les variables nécessaires pour notre base de test :
Ensuite, nous créons des scripts dans package.json
pour lancer nos tests d'intégration :
Notez qu'on a crée 2 scripts :
integration-no-env
: qui, commeunit
lance mocha normalementintegration
: qui va commencer par charger les variables d'environnement de./test/.env.test
avant de lancer le scriptintegration-no-env
On sépare ses deux scripts parce qu'à terme, on va pouvoir préciser ces variables d'environnement dans un fichier externe (un docker-composer.yml
par exemple).
Pour lancer le test en local, on va devoir d'abord installer le package env-cmd
:
On est enfin prêt à lancer notre test d'intégration :
... qui donnera le résultat suivant :
Considérations
Les tests d'intégration, surtout avec une base de données, peuvent-être assez compliqué à mettre en place :
Qu'elles sont les données à charger (préconditions) avant l'exécution de mon test ? Parfois, elles en sont nombreuses. Pour l'exemple de publicité, il faut d'abord un annonceur, un éditeur, un utilisateur, une publicité. Il faut créer toutes ces données, et les importer dans votre base avant de lancer le test. Ceci pourrait être :
dans les scripts d'initialisation par exemple (
before
hook de mocha)via des modules utilitaires qui permettent de créer tout le scenario
une combinaison des deux
Occasionnellement, on aimerait interroger directement la base de données pour valider que les bonnes données y sont mises. Il faudrait peut-être ajouter à la classe
RootDB.ts
des fonctions utiles pour ce faire.Attention au TEMPS et aux DATES ! Si votre application utilise la notion de temps, il faut bien concevoir vos tests pour se passer à un moment fixe, sinon vos tests ne fonctionneront plus dans le futur. En revanche, cela veut dire qu'il y ait la possibilité de paramétrer la date/temps de votre plateforme de façon globale.
Performance : attention à ne pas importer toute une base de production avant chaque test. On ne veut pas que les tests soient trop longs !
Code coverage
On aimerait savoir si on a testé toutes les lignes de code dans notre projet.
Et, si on oublie une condition particulière, et on n'a pas un test pour cela ?
Heureusement il y a des outils qui permettent de nous indiquer si nos tests ont bien couvert toutes les différentes branches possibles de notre projet.
Nous allons utiliser le package istanbul
(ou nyc
) :
Nous allons mettre à jour notre package.json
afin d'invoquer cet outil et le paramétrer :
À noter, nous avons ajouté le prefixe nyc --report-dir ./coverage/[DOSSIER]
ainsi que l'option -r source-map-support/register --recursive
à nos 2 lignes de test.
A la fin du fichier package.json
, on ajoute une section dédiée à nyc
:
On précise de regarder uniquement les fichiers .ts
, et de nous générer 3 types de rapport :
text
: En texte pour chaque fichiertext-summary
: Un résumé de tous les testscobertura
: Un rapport en XML qu'on va utiliser plus tard pour nos processus de CI/CD
Si on relance npm run unit
on aura le résultat :
On voit qu'il y a des fichiers dont on n'a pas forcément touché à toutes les lignes de code dans nos tests.
Essayez avec npm run integration
.
Est-ce que vous avez remarqué qu'on n'a pas forcément testé les comportements de l'API ? C'est-à-dire, tester qu'on récupère les bons codes HTTP dans nos réponses, etc. Nous nous en occupons dans l'étape suivante !
Dernière mise à jour