L'auteur a choisi le Open Internet/Free Speech Fund pour recevoir un don dans le cadre du programme Write for DOnations.

Introduction

Les tests font partie intégrante du développement de logiciels. Il est courant pour les programmeurs d'exécuter un code qui teste leur application lorsqu'ils apportent des modifications, afin de confirmer qu'elle se comporte comme ils le souhaitent. Avec la bonne configuration de test, ce processus peut même être automatisé, ce qui permet de gagner beaucoup de temps. L'exécution régulière de tests après l'écriture d'un nouveau code permet de s'assurer que les modifications ne cassent pas les fonctionnalités préexistantes. Cela permet d'accroître la confiance qu'ont les développeurs dans leur base de code, surtout lorsqu'elle est déployée en production pour que les utilisateurs puissent interagir avec elle.

Un framework de test structure la manière dont nous créons les cas de test. Mocha est un framework de test JavaScript populaire qui organise nos cas de test et les exécute pour nous. Cependant, Mocha ne vérifie pas le comportement de notre code. Pour comparer les valeurs dans un test, nous pouvons utiliser le module Node.js assert.

Dans cet article, vous allez écrire des tests pour un module de liste TODO (À faire) de Node.js. Vous configurerez et utiliserez le framework de test Mocha pour structurer vos tests. Ensuite, vous utiliserez le module Node.js assert pour créer les tests eux-mêmes. En ce sens, vous utiliserez Mocha comme constructeur de plan, et assert pour implémenter le plan.

Conditions préalables

Étape 1 — Écriture d'un module Node

Commençons cet article par l'écriture du module Node.js que nous aimerions tester. Ce module permet de gérer une liste d'éléments TODO. Grâce à ce module, nous pourrons dresser la liste de toutes les choses à faire que nous suivons, ajouter de nouveaux éléments et marquer certains comme terminés. De plus, nous pourrons exporter une liste d'éléments TODO vers un fichier CSV. Si vous souhaitez un rappel sur l'écriture des modules Node.js, vous pouvez lire notre article Comment créer un module Node.js.

Tout d'abord, nous devons configurer l'environnement de codage. Créez un dossier avec le nom de votre projet dans votre terminal. Ce tutoriel utilisera le nom todos :

  • mkdir todos

Entrez ensuite dans ce dossier :

  • cd todos

Initialisez maintenant npm, car nous utiliserons plus tard sa fonctionnalité CLI pour effectuer les tests :

  • npm init -y

Nous n'avons qu'une seule dépendance, Mocha, que nous utiliserons pour organiser et exécuter nos tests. Pour télécharger et installer Mocha, utilisez ce qui suit :

  • npm i request --save-dev mocha

Nous installons Mocha comme une dépendance dev, car il n'est pas requis par le module dans un environnement de production. Si vous souhaitez en savoir plus sur les packages Node.js ou sur npm, consultez notre guide Comment utiliser les modules Node.js avec npm et package.json.

Enfin, créons le fichier qui contiendra le code de notre module :

  • touch index.js

Une fois que cela est fait, nous sommes prêts à créer notre module. Ouvrez index.js dans un éditeur de texte comme nano :

  • nano index.js

Commençons par définir la classe Todos. Cette classe contient toutes les fonctions dont nous avons besoin pour gérer notre liste TODO. Ajoutez les lignes de code suivantes à index.js :

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }
}

module.exports = Todos;

Nous commençons le fichier en créant une classe Todos. Sa fonction constructor() ne prend aucun argument, donc nous n'avons pas besoin de fournir de valeurs pour instancier un objet pour cette classe. Tout ce que nous faisons lorsque nous initialisons un objet Todos est de créer une propriété todos qui est un tableau vide.

La ligne modules permet aux autres modules Node.js d'exiger notre classe Todos. Si nous n'exportons pas explicitement la classe, le fichier de test que nous créerons plus tard ne pourra pas l'utiliser.

Ajoutons une fonction pour retourner le tableau des todos que nous avons stocké. Écrivez les lignes surlignées suivantes :

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }
}

module.exports = Todos;

Notre fonction list() renvoie une copie du tableau qui est utilisé par la classe. Elle réalise une copie du tableau en utilisant la syntaxe de déstructuration de JavaScript. Nous faisons une copie du tableau de sorte que les modifications apportées par l'utilisateur au tableau renvoyé par list() n'affectent pas le tableau utilisé par l'objet Todos.

Remarque : les tableaux JavaScript sont des types de références. Cela signifie que pour toute affectation de variable à un tableau ou toute invocation de fonction avec un tableau comme paramètre, JavaScript se réfère au tableau original qui a été créé. Par exemple, si nous avons un tableau avec trois éléments appelés x, et que nous créons une nouvelle variable y telle que y = x, y et x se réfèrent tous deux à la même chose. Toute modification apportée au tableau avec y a une incidence sur la variable x et vice versa.

Écrivons maintenant la fonction add(), qui ajoute un nouvel élément TODO :

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }

        this.todos.push(todo);
    }
}

module.exports = Todos;

Notre fonction add() prend une chaîne et la place dans la propriété title d'un nouvel objet JavaScript. Le nouvel objet a également une propriété completed, qui est réglée sur false par défaut. Nous ajoutons ensuite ce nouvel objet à notre tableau de TODO.

L'une des fonctionnalités importantes dans un gestionnaire TODO est la capacité de marquer les éléments comme étant terminés. Pour cette implémentation, nous passerons en boucle sur notre tableau todos pour trouver l'élément TODO que l'utilisateur recherche. Si l'élément est trouvé, nous le marquerons comme terminé. Si l'élément n'est pas trouvé, nous lancerons une erreur.

Ajoutez la fonction complete() comme ceci :

todos/index.js
class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }

        this.todos.push(todo);
    }

    complete(title) {
        let todoFound = false;
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }
}

module.exports = Todos;

Sauvegardez le fichier et quittez l'éditeur de texte.

Nous avons maintenant un gestionnaire TODO de base avec lequel nous pouvons expérimenter. Ensuite, nous allons tester manuellement notre code pour voir si l'application fonctionne.

Étape 2 — Test manuel du code

Dans cette étape, nous allons exécuter les fonctions de notre code et observer les résultats pour nous assurer qu'ils correspondent à nos attentes. C'est ce qu'on appelle le test manuel. Il s'agit probablement de la méthode de test la plus couramment appliquée par les programmeurs. Même si nous automatiserons nos tests plus tard avec Mocha, nous commencerons par tester manuellement notre code pour mieux comprendre la différence entre les tests manuels et les frameworks de test.

Ajoutons deux éléments TODO à notre application et marquons-en un comme étant terminé. Lancez le Node.js REPL dans le même dossier que le fichier index.js :

  • node

Vous verrez l'invite > dans le REPL qui nous indique que nous pouvons entrer le code JavaScript. Saisissez ce qui suit à l'invite :

  • const Todos = require('./index');

Avec require(), nous chargeons le module TODO dans une variable Todos. Rappelons que notre module renvoie la classe Todos par défaut.

Maintenant, instancions un objet pour cette classe. Dans le REPL, ajoutez cette ligne de code :

  • const todos = new Todos();

Nous pouvons utiliser l'objet todos pour vérifier que notre implémentation fonctionne. Ajoutons notre premier élément TODO :

  • todos.add("run code");

Jusqu'à présent, nous n'avons pas vu de sortie dans notre terminal. Vérifions que nous avons stocké notre élément TODO "run code" en obtenant une liste de tous nos éléments TODO :

  • todos.list();

Vous verrez cette sortie dans votre REPL :

Output
[ { title: 'run code', completed: false } ]

Ceci est le résultat attendu : nous avons un élément TODO dans notre tableau des TODO, et il n'est pas terminé par défaut.

Ajoutons un autre élément TODO :

  • todos.add("test everything");

Marquez le premier élément TODO comme étant terminé :

  • todos.complete("run code");

Notre objet todos va maintenant gérer deux éléments : "run code" et "test everything". Le TODO "run code" sera également terminé. Confirmons cela en appelant une nouvelle fois list() :

  • todos.list();

Le REPL produira la sortie :

Output
[ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]

Maintenant, quittez le REPL avec ce qui suit :

  • .exit

Nous avons confirmé que notre module se comporte comme nous nous y attendions. Bien que nous n'ayons pas mis notre code dans un fichier de test ou utilisé une bibliothèque de test, nous avons testé notre code manuellement. Malheureusement, cette façon de tester prend beaucoup de temps à chaque fois que nous effectuons une modification. Utilisons maintenant les tests automatisés dans Node.js et voyons si nous pouvons résoudre ce problème avec le framework de test Mocha.

Étape 3 - Écriture de votre premier test avec Mocha et Assert

Dans l'étape précédente, nous avons testé notre application manuellement. Cela fonctionne pour les cas d'utilisation individuels, mais à mesure que notre module évolue, cette méthode devient moins viable. Lorsque nous testons de nouvelles fonctions, nous devons nous assurer que la fonctionnalité ajoutée n'a pas créé de problèmes dans l'ancienne fonctionnalité. Nous souhaitons tester chaque fonction pour chaque modification du code, mais le faire manuellement demanderait beaucoup d'efforts et serait sujet à des erreurs.

Mettre en place des tests automatisés constitue une pratique plus efficace. Il s'agit de scripts de test écrits comme tout autre bloc de code. Nous exécutons nos fonctions avec des entrées définies et inspectons leurs effets pour nous assurer qu'elles se comportent comme nous l'attendons. Au fur et à mesure que notre base de code s'accroît, nos tests automatisés s'étendent également. Lorsque nous écrivons de nouveaux tests pour les nouvelles fonctionnalités, nous pouvons vérifier que l'ensemble du module fonctionne toujours, sans avoir à nous rappeler à chaque fois comment utiliser chaque fonction.

Dans ce tutoriel, nous utilisons le framework de test Mocha avec le module Node.js assert. Voyons en pratique comment ils fonctionnent ensemble.

Pour commencer, créez un fichier pour stocker notre code test :

  • touch index.test.js

Utilisez maintenant votre éditeur de texte préféré pour ouvrir le fichier test. Vous pouvez utiliser nano comme auparavant :

  • nano index.test.js

Dans la première ligne du fichier texte, nous chargerons le module TODO comme nous l'avons fait dans le shell Node.js. Nous chargerons ensuite le module assert lorsque nous écrirons nos tests. Ajoutez les lignes suivantes :

todos/index.test.js
const Todos = require('./index');
const assert = require('assert').strict;

La propriété strict du module assert nous permettra d'utiliser des tests d'égalité spéciaux recommandés par Node.js, qui nous aideront à préparer l'avenir, car ils prennent en compte un plus grand nombre de cas d'utilisation.

Avant de commencer à écrire les tests, voyons comment Mocha organise notre code. Les tests structurés en Mocha suivent généralement ce modèle :

describe([String with Test Group Name], function() {
    it([String with Test Name], function() {
        [Test Code]
    });
});

Remarquez deux fonctions clés : describe() et it(). La fonction describe() est utilisée pour regrouper des tests similaires. Mocha n'a pas besoin de cette fonction pour exécuter les tests, mais le regroupement des tests rend notre code de test plus facile à maintenir. Il est recommandé de regrouper vos tests de manière à pouvoir mettre à jour facilement les tests similaires.

La fonction it() contient notre code de test. C'est là que nous pouvons interagir avec les fonctions de notre module et utiliser la bibliothèque assert. De nombreuses fonctions it() peuvent être définies dans une fonction describe().

Notre but dans cette section est d'utiliser Mocha et assert pour automatiser notre test manuel. Nous allons procéder étape par étape, en commençant par notre bloc de description. Ajoutez ce qui suit à votre fichier après les lignes de module :

todos/index.test.js
...
describe("integration test", function() {
});

Avec ce bloc de code, nous avons créé un regroupement pour nos tests intégrés. Les tests unitaires permettent de tester une fonction à la fois. Les tests d'intégration permettent de vérifier le bon fonctionnement global des fonctions au sein des modules ou entre les modules. Lorsque Mocha effectuera notre test, tous les tests de ce bloc de description seront effectués dans le groupe "integration test".

Ajoutons une fonction it() pour pouvoir commencer à tester le code de notre module :

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
    });
});

Remarquez à quel point nous avons rendu le nom du test descriptif. Si quelqu'un exécute notre test, il saura immédiatement ce qui a réussi ou échoué. Une application bien testée est généralement une application bien documentée, et les tests peuvent parfois constituer un type de documentation efficace.

Pour notre premier test, nous allons créer un nouvel objet Todos et vérifier qu'il ne contient aucun élément :

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.notStrictEqual(todos.list().length, 1);
    });
});

La première nouvelle ligne de code a instancié un nouvel objet Todos comme nous le ferions dans le REPL Node.js ou un autre module. Dans la deuxième nouvelle ligne, nous utilisons le module assert.

À partir du module assert, nous utilisons la méthode notStrictEqual(). Cette fonction prend deux paramètres : la valeur que nous voulons tester (appelée actual, valeur réelle) et la valeur que nous nous attendons à obtenir (appelée expected, valeur attendue). Si les deux arguments sont identiques, notStrictEqual() lance une erreur pour faire échouer le test.

Enregistrez et quittez index.test.js.

Le cas de base sera vrai car la longueur doit être de 0, ce qui n'est pas 1. Confirmons cela en exécutant Mocha. Pour ce faire, nous devons modifier notre fichier package.json. Ouvrez votre fichier package.json avec votre éditeur de texte :

  • nano package.json

Maintenant, dans votre propriété scripts, changez le contenu pour qu'il ressemble à ceci :

todos/package.json
...
"scripts": {
    "test": "mocha index.test.js"
},
...

Nous venons de modifier le comportement de la commande test de la CLI de npm. Lorsque nous exécuterons npm test, npm passera en revue la commande que nous venons d'entrer dans package.json. Il cherchera la bibliothèque Mocha dans notre dossier node_modules et exécutera la commande mocha avec notre fichier test.

Enregistrez et quittez package.json.

Voyons ce qui se passe lorsque nous exécutons notre test. Dans votre terminal, entrez :

  • npm test

La commande produira la sortie suivante :

Output
> todos@1.0.0 test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

Cette sortie nous montre d'abord le groupe de tests qui va être exécuté. Pour chaque test individuel au sein d'un groupe, le cas de test est en retrait. Nous voyons notre nom de test tel que nous l'avons décrit dans la fonction it(). La coche sur le côté gauche du cas de test indique que le test a réussi.

En bas, nous obtenons un résumé de tous nos tests. Dans notre cas, notre test unique a réussi et a été effectué en 16 ms (la durée varie d'un ordinateur à l'autre).

Nos tests ont commencé avec succès. Cependant, ce cas de test peut permettre des faux positifs. Un faux positif est un cas de test qui réussit alors qu'il devrait échouer.

Nous vérifions actuellement que la longueur du tableau n'est pas égale à 1. Modifions le test pour que cette condition se vérifie alors qu'elle ne devrait pas. Ajoutez les lignes suivantes à index.test.js :

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.notStrictEqual(todos.list().length, 1);
    });
});

Enregistrez et quittez le fichier.

Nous avons ajouté deux éléments TODO. Exécutons le test pour voir ce qu'il se passe :

  • npm test

Cela donnera le résultat :

Output
... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)

Il réussit conformément aux attentes, puisque la longueur est supérieure à 1, mais il va à l'encontre de l'objectif initial de ce premier test. Le premier test est destiné à confirmer que nous partons avec un objet vide. Un meilleur test permettra de confirmer cela dans tous les cas.

Modifions le test pour qu'il ne réussisse que si nous n'avons absolument aucun TODO en stock. Apportez les modifications suivantes à index.test.js :

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.strictEqual(todos.list().length, 0);
    });
});

Vous avez modifié notStrictEqual() en strictEqual(), une fonction qui vérifie l'égalité entre son argument réel et son argument attendu. L'égalité stricte échouera si nos arguments ne sont pas exactement les mêmes.

Enregistrez et quittez, puis exécutez le test pour que nous puissions voir ce qu'il se passe :

  • npm test

Cette fois-ci, la sortie affichera une erreur :

Output
... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.

Ce texte nous sera utile pour comprendre les raisons de l'échec du test. Notez que puisque le test a échoué, il n'y avait pas de coche avant le cas de test.

Notre résumé de test n'est plus au bas de la sortie, mais juste après l'affichage de notre liste de cas de test :

...
0 passing (29ms)
  1 failing
...

La suite de la sortie nous fournit des données sur nos tests échoués. Tout d'abord, nous voyons quel cas de test a échoué :

...
1) integrated test
       should be able to add and complete TODOs:
...

Ensuite, nous voyons pourquoi notre test a échoué :

...
      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
+ expected - actual

- 2
+ 0
      + expected - actual

      -2
      +0

      at Context.<anonymous> (index.test.js:9:10)
...

Une erreur AssertionError est lancée lorsque strictEqual() échoue. Nous voyons que la valeur expected, 0, est différente de la valeur actual, 2.

Nous voyons alors dans notre fichier test la ligne où le code échoue. Dans ce cas, c'est la ligne 10.

Nous avons ainsi vu par nous-mêmes que notre test échouera si nous nous attendons à des valeurs incorrectes. Remettons notre cas de test à sa juste valeur. Tout d'abord, ouvrez le fichier :

  • nano index.test.js

Retirez ensuite les lignes todos.add pour que votre code ressemble à ce qui suit :

todos/index.test.js
...
describe("integration test", function () {
    it("should be able to add and complete TODOs", function () {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);
    });
});

Enregistrez et quittez le fichier.

Exécutez-le une fois de plus pour confirmer qu'il réussit sans faux positif potentiel :

  • npm test

La sortie sera la suivante :

Output
... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)

Nous avons désormais bien amélioré la résilience de notre test. Continuons avec notre test d'intégration. L'étape suivante consiste à ajouter un nouvel élément TODO à index.test.js

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);

        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
    });
});

Après avoir utilisé la fonction add(), nous confirmons que nous avons maintenant un TODO géré par notre objet todos avec strictEqual(). Notre prochain test confirme les données dans les todos avec deepStrictEqual(). La fonction deepStrictEqual() teste récursivement que nos objets attendus et réels ont les mêmes propriétés. Dans le cas présent, elle vérifie que les tableaux que nous attendons ont tous deux un objet JavaScript en leur sein. Elle vérifie ensuite que leurs objets JavaScript ont les mêmes propriétés, c'est-à-dire que leurs propriétés title sont toutes deux "run code" et que les deux propriétés completed sont false.

Nous terminons ensuite les tests restants en utilisant ces deux contrôles d'égalité selon les besoins, en ajoutant les lignes surlignées suivantes :

todos/index.test.js
...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);

        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);

        todos.add("test everything");
        assert.strictEqual(todos.list().length, 2);
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: false },
                { title: "test everything", completed: false }
            ]
        );

        todos.complete("run code");
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: true },
                { title: "test everything", completed: false }
            ]
    );
  });
});

Enregistrez et quittez le fichier.

Notre test reproduit maintenant notre test manuel. Grâce à ces tests programmatiques, nous n'avons pas besoin de vérifier continuellement les sorties si nos tests réussissent lorsque nous les effectuons. Il est conseillé de tester chaque aspect de l'utilisation pour vous assurer que le code est correctement testé.

Exécutons une fois encore notre npm test pour obtenir cette sortie familière :

Output
... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)

Vous avez désormais configuré un test intégré avec le framework Mocha et la bibliothèque assert.

Imaginons une situation dans laquelle nous avons partagé notre module avec d'autres développeurs et que ceux-ci nous donnent leur avis maintenant. Une bonne partie de nos utilisateurs voudraient que la fonction complete() renvoie une erreur si aucun TODO n'a encore été ajouté. Ajoutons cette fonctionnalité à notre fonction complete().

Ouvrez index.js dans votre éditeur de texte :

  • nano index.js

Ajoutez ce qui suit à la fonction :

todos/index.js
...
complete(title) {
    if (this.todos.length === 0) {
        throw new Error("You have no TODOs stored. Why don't you add one first?");
    }

    let todoFound = false
    this.todos.forEach((todo) => {
        if (todo.title === title) {
            todo.completed = true;
            todoFound = true;
            return;
        }
    });

    if (!todoFound) {
        throw new Error(`No TODO was found with the title: "${title}"`);
    }
}
...

Enregistrez et quittez le fichier.

Ajoutons maintenant un nouveau test pour cette nouvelle fonctionnalité. Nous voulons vérifier que si nous appelons la fonction complete sur un objet Todos qui n'a pas d'éléments, il nous renverra notre erreur spéciale.

Retournez dans index.test.js :

  • nano index.test.js

À la fin du fichier, ajoutez le code suivant :

todos/index.test.js
...
describe("complete()", function() {
    it("should fail if there are no TODOs", function() {
        let todos = new Todos();
        const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");

        assert.throws(() => {
            todos.complete("doesn't exist");
        }, expectedError);
    });
});

Nous utilisons describe() et it() comme auparavant. Notre test commence par la création d'un nouvel objet todos. Nous définissons ensuite l'erreur que nous nous attendons à recevoir lorsque nous appelons la fonction complete().

Ensuite, nous utilisons la fonction throws() du module assert. Cette fonction a été créée pour que nous puissions vérifier les erreurs qui sont lancées dans notre code. Son premier argument est une fonction qui contient le code qui lance l'erreur. Le deuxième argument est l'erreur que nous nous attendons à recevoir.

Dans votre terminal, exécutez les tests avec npm test une fois encore et vous verrez maintenant le résultat suivant :

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)

Cette sortie met en évidence l'intérêt de faire des tests automatisés avec Mocha et assert. Grâce à nos scripts de tests, chaque fois que nous exécutons npm test, nous vérifions que tous nos tests sont réussis. Nous n'avons pas eu besoin de vérifier manuellement si l'autre code fonctionne toujours : nous savons que c'est le cas, parce que le test a encore réussi.

Jusqu'à présent, nos tests ont permis de vérifier les résultats de code synchrone. Voyons comment nous devrions adapter nos nouvelles habitudes de test pour travailler avec du code asynchrone.

Étape 4 — Test de code asynchrone

L'une des caractéristiques que nous voulons dans notre module TODO est une fonction d'exportation CSV. Cela permettra d'imprimer dans un fichier tous les TODO que nous avons ainsi que leur état d'avancement. Pour cela, nous devons utiliser le module fs, un module Node.js intégré pour travailler avec le système de fichiers.

Écrire dans un fichier est une opération asynchrone. Il existe de nombreuses façons d'écrire dans un fichier dans Node.js. Nous pouvons utiliser les rappels, les promesses ou les mots-clés async/await. Dans cette section, nous allons voir comment nous écrivons des tests pour ces différentes méthodes.

Rappels

Une fonction de rappel ou callback est une fonction qui sert d'argument à une fonction asynchrone. Elle est appelée quand l'opération asynchrone est terminée.

Ajoutons une fonction appelée saveToFile() à notre classe Todos. Cette fonction permet de construire une chaîne en passant en boucle tous nos articles TODO et en écrivant cette chaîne dans un fichier.

Ouvrez votre fichier index.js :

  • nano index.js

Dans ce fichier, ajoutez le code surligné suivant :

todos/index.js
const fs = require('fs');

class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }
        this.todos.push(todo);
    }

    complete(title) {
        if (this.todos.length === 0) {
            throw new Error("You have no TODOs stored. Why don't you add one first?");
        }

        let todoFound = false
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }

    saveToFile(callback) {
        let fileContents = 'Title,Completed\n';
        this.todos.forEach((todo) => {
            fileContents += `${todo.title},${todo.completed}\n`
        });

        fs.writeFile('todos.csv', fileContents, callback);
    }
}

module.exports = Todos;

Nous devons d'abord importer le module fs dans notre fichier. Nous ajoutons ensuite notre nouvelle fonction saveToFile(). Notre fonction prend une fonction de rappel qui sera utilisée une fois l'opération d'écriture du fichier terminée. Dans cette fonction, nous créons une variable fileContents qui stocke toute la chaîne que nous voulons enregistrer en tant que fichier. Elle est initialisée avec les en-têtes CSV. Nous passons ensuite en boucle chaque élément TODO avec la méthode forEach() du tableau interne. Au fur et à mesure de l'itération, nous ajoutons les propriétés title et completed des objets todos individuels.

Enfin, nous utilisons le module fs pour écrire le fichier avec la fonction writeFile(). Notre premier argument est le nom du fichier : todos.csv. Le second est le contenu du fichier, dans ce cas, notre variable fileContents. Notre dernier argument est notre fonction de rappel, qui gère toute erreur d'écriture de fichier.

Enregistrez et quittez le fichier.

Nous allons maintenant écrire un test pour notre fonction saveToFile(). Notre test aura deux objectifs : confirmer l'existence du fichier, puis vérifier qu'il a le bon contenu.

Ouvrez le fichier index.test.js :

  • nano index.test.js

Commençons par charger le module fs en haut du fichier, car nous l'utiliserons pour tester nos résultats :

todos/index.test.js
const Todos = require('./index');
const assert = require('assert').strict;
const fs = require('fs');
...

Maintenant, à la fin du fichier, ajoutons notre nouveau cas de test :

todos/index.test.js
...
describe("saveToFile()", function() {
    it("should save a single TODO", function(done) {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.saveToFile((err) => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
            done(err);
        });
    });
});

Comme auparavant, nous utilisons describe() pour grouper notre test séparément des autres car il implique de nouvelles fonctionnalités. La fonction it() est légèrement différente de nos autres fonctions. Habituellement, la fonction de rappel que nous utilisons n'a pas d'arguments. Cette fois-ci, nous avons l'argument done. Nous avons besoin de cet argument lorsque nous testons des fonctions avec des rappels. La fonction de rappel done() est utilisée par Mocha pour savoir quand une fonction asynchrone est terminée.

Toutes les fonctions de rappel testées dans Mocha doivent appeler le rappel done(). Sinon, Mocha ne saurait jamais quand la fonction est terminée et serait coincé dans l'attente d'un signal.

En continuant, nous créons notre instance Todos et y ajoutons un seul élément. Nous appelons ensuite la fonction saveToFile(), avec un rappel qui capture une erreur d'écriture de fichier. Notez que notre test pour cette fonction réside dans le rappel. Si notre code de test était en dehors du rappel, il échouerait tant que le code serait appelé avant que l'écriture du fichier ne soit terminée.

Dans notre fonction de rappel, nous vérifions d'abord que notre fichier existe :

todos/index.test.js
...
assert.strictEqual(fs.existsSync('todos.csv'), true);
...

La fonction fs.existsSync() renvoie true si le chemin du fichier dans son argument existe, sinon, elle renvoie false.

Remarque : les fonctions du module fs sont asynchrones par défaut. Cependant, pour les fonctions clés, elles font des contreparties synchrones. Ce test est plus simple en utilisant des fonctions synchrones, car nous n'avons pas besoin d'imbriquer le code asynchrone pour nous assurer qu'il fonctionne. Dans le module fs, les fonctions synchrones se terminent généralement par "Sync" à la fin de leur nom.

Nous créons ensuite une variable pour stocker notre valeur attendue :

todos/index.test.js
...
let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
...

Nous utilisons readFileSync() du module fs pour lire le fichier de manière synchrone :

todos/index.test.js
...
let content = fs.readFileSync("todos.csv").toString();
...

Nous fournissons maintenant à readFileSync() le bon chemin pour le fichier : todos.csv. Comme readFileSync() renvoie un objet Buffer, qui stocke des données binaires, nous utilisons sa méthode toString() afin de pouvoir comparer sa valeur avec la chaîne que nous nous attendons à avoir sauvegardée.

Comme auparavant, nous utilisons le module strictEqual d’assert pour faire une comparaison :

todos/index.test.js
...
assert.strictEqual(content, expectedFileContents);
...

Nous terminons notre test en appelant le rappel done(), nous assurant ainsi que Mocha sait qu'il doit arrêter de tester ce cas :

todos/index.test.js
...
done(err);
...

Nous fournissons l'objet err à done() pour que Mocha puisse indiquer faire échouer le test dans le cas où une erreur se produirait.

Enregistrez et quittez index.test.js.

Exécutons ce test avec npm test comme auparavant. Votre console affichera cette sortie :

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)

Vous avez désormais testé votre première fonction asynchrone avec Mocha en utilisant des rappels. Mais au moment de la rédaction de ce tutoriel, les promesses sont plus fréquentes que les rappels dans le nouveau code Node.js, comme expliqué dans notre article Comment écrire du code asynchrone dans Node.js. Maintenant, apprenons à les tester avec Mocha également.

Promesses

Une promesse ou Promise est un objet JavaScript qui renverra finalement une valeur. Lorsqu'une promesse est réussie, elle est résolue. Lorsqu'elle rencontre une erreur, elle est rejetée.

Modifions la fonction saveToFile() pour qu'elle utilise des promesses au lieu de rappels. Ouvrez index.js :

  • nano index.js

Tout d'abord, nous devons modifier la façon dont le module fs est chargé. Dans votre fichier index.js, modifiez l'instruction require() en haut du fichier pour qu'elle ressemble à ceci :

todos/index.js
...
const fs = require('fs').promises;
...

Nous venons d'importer le module fs qui utilise les promesses au lieu des rappels. Maintenant, nous devons apporter quelques modifications à la fonction saveToFile() pour qu'elle fonctionne avec les promesses.

Dans votre éditeur de texte, apportez les modifications suivantes à la fonction saveToFile() pour supprimer les rappels :

todos/index.js
...
saveToFile() {
    let fileContents = 'Title,Completed\n';
    this.todos.forEach((todo) => {
        fileContents += `${todo.title},${todo.completed}\n`
    });

    return fs.writeFile('todos.csv', fileContents);
}
...

La première différence est que notre fonction n'accepte plus aucun argument. Avec les promesses, nous n'avons pas besoin d'une fonction de rappel. Le deuxième changement concerne la manière dont le fichier est rédigé. Nous retournons maintenant le résultat de la promesse writeFile().

Enregistrez et fermez index.js.

Nous allons maintenant adapter notre test pour qu'il fonctionne avec les promesses. Ouvrez index.test.js :

  • nano index.test.js

Modifiez ainsi le test saveToFile() :

todos/index.js
...
describe("saveToFile()", function() {
    it("should save a single TODO", function() {
        let todos = new Todos();
        todos.add("save a CSV");
        return todos.saveToFile().then(() => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
        });
    });
});

La première modification que nous devons apporter consiste à supprimer le rappel done() de ses arguments. Si Mocha passe l'argument done(), il doit être appelé ou il lancera une erreur comme celle-ci :

1) saveToFile()
       should save a single TODO:
     Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
      at listOnTimeout (internal/timers.js:536:17)
      at processTimers (internal/timers.js:480:7)

Lorsque vous testez les promesses, n'incluez pas le rappel done() à it().

Pour tester notre promesse, nous devons mettre notre code d'assertion dans la fonction then(). Notez que nous retournons cette promesse dans le test, et que nous n'avons pas de fonction catch() à attraper lorsque la Promise est rejetée.

Nous retournons la promesse de manière à ce que toutes les erreurs qui sont lancées dans la fonction then() soient reportées dans la fonction it(). Si les erreurs ne ressortent pas, Mocha ne fera pas échouer le cas type. Lorsque vous testez des promesses, vous devez utiliser return sur la Promise testée. Sinon, vous courez le risque d'obtenir un faux-positif.

Nous omettons également la clause catch() car Mocha peut détecter quand une promesse est rejetée. Si elle est rejetée, Mocha fait automatiquement échouer le test.

Maintenant que notre test est en place, sauvegardez et quittez le fichier, puis exécutez Mocha avec npm test pour confirmer que nous avons obtenu un résultat positif :

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)

Nous avons modifié notre code et nos tests pour utiliser les promesses, et nous savons maintenant avec certitude que cela fonctionne. Mais les modèles asynchrones les plus récents utilisent les mots-clés async/await, de sorte que nous n'avons pas à créer plusieurs fonctions then() pour gérer les résultats. Voyons comment nous pouvons tester avec async/await.

async/await

Les mots-clés async/await rendent le travail avec les promesses moins verbeux. Une fois que nous avons défini une fonction comme étant asynchrone avec le mot-clé async, nous pouvons obtenir tout résultat futur dans cette fonction avec le mot-clé await. De cette façon, nous pouvons utiliser les promesses sans avoir à utiliser les fonctions then() ou catch().

Nous pouvons simplifier notre test saveToFile() basé sur une promesse avec async/await. Dans votre éditeur de texte, effectuez ces modifications mineures au test saveToFile() dans index.test.js :

todos/index.test.js
...
describe("saveToFile()", function() {
    it("should save a single TODO", async function() {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

Le premier changement est que la fonction utilisée par la fonction it() a maintenant le mot-clé async lorsqu'elle est définie. Cela nous permet d'utiliser le mot-clé await dans son corps.

Le second changement apparait lorsque nous appelons saveToFile(). Le mot-clé await est utilisé avant d'appeler cette fonction. Maintenant, Node.js sait qu'il faut attendre que cette fonction soit résolue avant de poursuivre le test.

Notre code de fonction est plus facile à lire maintenant que nous avons déplacé le code qui était dans la fonction then() vers le corps de la fonction it(). L'exécution de ce code avec npm test produit cette sortie :

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)

Nous pouvons désormais tester les fonctions asynchrones en utilisant l'un des trois paradigmes asynchrones de manière appropriée.

Nous avons déjà couvert beaucoup de sujets en testant le code synchrone et asynchrone avec Mocha. Découvrons maintenant d'un peu plus près d'autres fonctionnalités offertes par Mocha pour améliorer notre expérience de test, en particulier la façon dont les hooks peuvent changer les environnements de test.

Étape 5 — Utilisation des hooks pour améliorer les cas de test

Les hooks sont une fonctionnalité utile de Mocha qui nous permet de configurer l'environnement avant et après un test. Nous ajoutons généralement des hooks dans un bloc de fonction describe(), car ils contiennent une logique de configuration et de démontage spécifique à certains cas de test.

Mocha fournit quatre hooks que nous pouvons utiliser dans nos tests :

  • before : ce hook est exécuté avant que le premier test commence.
  • beforeEach : ce hook est exécuté avant chaque cas de test.
  • after : ce hook est exécuté après que le dernier cas de test est terminé.
  • afterEach : ce hook est exécuté après chaque cas de test.

Lorsque nous testons une fonction ou une fonctionnalité plusieurs fois, les hooks sont utiles, car ils nous permettent de séparer le code de configuration du test (comme la création de l'objet todos) du code d'assertion du test.

Pour évaluer la valeur des hooks, ajoutons d'autres tests à notre bloc de test saveToFile().

Bien que nous ayons confirmé que nous pouvons enregistrer nos éléments TODO dans un fichier, nous n'en avons enregistré qu'un seul. En outre, l'élément n'a pas été marqué comme étant terminé. Ajoutons d'autres tests pour nous assurer que les différents aspects de notre module fonctionnent.

Tout d'abord, ajoutons un second test pour confirmer que notre fichier est correctement enregistré lorsque nous avons un élément TODO terminé. Ouvrez le fichier index.test.js dans votre éditeur de texte :

  • nano index.test.js

Remplacez le dernier test par le suivant :

todos/index.test.js
...
describe("saveToFile()", function () {
    it("should save a single TODO", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.complete("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

Le test est semblable à celui que nous avions auparavant. Les principales différences sont que nous appelons la fonction complete() avant d'appeler saveToFile(), et que nos expectedFileContents ont maintenant la valeur true au lieu de false pour colonne completed.

Enregistrez et quittez le fichier.

Exécutons notre nouveau test, et tous les autres, avec npm test :

  • npm test

Cela donnera le résultat :

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)

Il fonctionne comme prévu. Il est toutefois possible de faire mieux. Ils doivent tous deux instancier un objet Todos au début du test. Au fur et à mesure que nous ajoutons des cas de test, cela devient rapidement répétitif et gourmand en mémoire. De plus, à chaque fois que nous effectuons le test, il crée un fichier. Cela peut être confondu avec une sortie réelle par une personne moins familière avec le module. Ce serait bien que nous nettoyions nos fichiers de sortie après le test.

Faisons ces améliorations en utilisant des hooks de test. Nous utiliserons le hook beforeEach() pour configurer notre fixture de test des objets TODO. Une fixture de test est tout état cohérent utilisé dans un test. Dans notre cas, notre fixture de test est un nouvel objet todos auquel un élément TODO a déjà été ajouté. Nous utiliserons ensuite afterEach() pour supprimer le fichier créé par le test.

Dans index.test.js, apportez les changements suivants à votre dernier test pour saveToFile() :

todos/index.test.js
...
describe("saveToFile()", function () {
    beforeEach(function () {
        this.todos = new Todos();
        this.todos.add("save a CSV");
    });

    afterEach(function () {
        if (fs.existsSync("todos.csv")) {
            fs.unlinkSync("todos.csv");
        }
    });

    it("should save a single TODO without error", async function () {
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync("todos.csv"), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        this.todos.complete("save a CSV");
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

Décomposons tous les changements que nous avons apportés. Nous avons ajouté un bloc beforeEach() au bloc de test :

todos/index.test.js
...
beforeEach(function () {
    this.todos = new Todos();
    this.todos.add("save a CSV");
});
...

Ces deux lignes de code créent un nouvel objet Todos qui sera disponible dans chacun de nos tests. Avec Mocha, l'objet this dans beforeEach() fait référence au mêmeobjet this dans it(). this est similaire pour chaque bloc de code à l'intérieur du bloc describe(). Pour plus d'informations sur this, consultez notre tutoriel Comprendre This, Bind, Call et Apply en JavaScript.

Ce puissant partage de contexte est la raison pour laquelle nous pouvons rapidement créer des fixtures de test qui fonctionnent pour nos deux tests.

Nous nettoyons ensuite notre fichier CSV dans la fonction afterEach() :

todos/index.test.js
...
afterEach(function () {
    if (fs.existsSync("todos.csv")) {
        fs.unlinkSync("todos.csv");
    }
});
...

Si notre test a échoué, il se peut qu'il n'ait pas créé de fichier. C'est pourquoi nous vérifions si le fichier existe avant d'utiliser la fonction unlinkSync() pour le supprimer.

Les modifications restantes font passer la référence de todos, qui était précédemment créée dans la fonction it(), à this.todos qui est disponible dans le contexte Mocha. Nous avons également supprimé les lignes qui instanciaient auparavant todos dans les cas de test individuels.

Maintenant, exécutons ce fichier pour confirmer que nos tests fonctionnent toujours. Entrez npm test dans votre terminal pour obtenir :

Output
... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)

Les résultats sont les mêmes, et en outre, nous avons légèrement réduit le temps de préparation des nouveaux tests pour la fonction saveToFile() et trouvé une solution au problème du fichier CSV résiduel.

Conclusion

Dans ce tutoriel, vous avez écrit un module Node.js pour gérer les éléments TODO et vous avez testé le code manuellement à l'aide du REPL Node.js. Vous avez ensuite créé un fichier de test et utilisé le framework Mocha pour effectuer des tests automatisés. Avec le module assert, vous avez pu vérifier que votre code fonctionne. Vous avez également testé des fonctions synchrones et asynchrones avec Mocha. Enfin, vous avez créé des hooks avec Mocha qui rendent l'écriture de plusieurs cas de test liés beaucoup plus lisible et facile à mettre à jour.

Muni de ces compétences, mettez-vous au défi d'écrire des tests pour les nouveaux modules Node.js que vous créez. Pouvez-vous réfléchir aux entrées et sorties de votre fonction et écrire votre test avant d'écrire votre code ?

Si vous souhaitez obtenir plus d'informations sur le framework de test Mocha, consultez la documentation officielle Mocha. Si vous souhaitez continuer à apprendre le fonctionnement de Node.js, vous pouvez retourner à la page de la série Comment coder en Node.js.

0 Comments

Creative Commons License