One place for hosting & domains

      Mocha

      Comment tester un module Node.js avec Mocha et Assert


      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 :

      Entrez ensuite dans ce dossier :

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

      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 :

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

      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 :

      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 :

      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 :

      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() :

      Le REPL produira la sortie :

      Output

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

      Maintenant, quittez le REPL avec ce qui suit :

      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 :

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

      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 :

      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 :

      La commande produira la sortie suivante :

      Output

      > [email protected] 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 :

      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 :

      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 :

      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 :

      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 :

      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 :

      À 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 :

      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,Completedn';
              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 :

      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,Completednsave a CSV,falsen";
                  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,Completednsave a CSV,falsen";
      ...
      

      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 :

      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,Completedn';
          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 :

      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,Completednsave a CSV,falsen";
                  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,Completednsave a CSV,falsen";
              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 :

      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,Completednsave a CSV,falsen";
              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,Completednsave a CSV,truen";
              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 :

      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,Completednsave a CSV,falsen";
              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,Completednsave a CSV,truen";
              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.



      Source link

      Тестирование модуля Node.js с использованием Mocha и Assert


      Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.

      Введение

      Тестирование является неотъемлемой частью разработки программного обеспечения. Обычно программисты запускают код, который тестирует разработанные ими приложения, при внесении каких-либо изменений, чтобы убедиться, что все работает, как надо. При правильных тестовых настройках этот процесс можно автоматизировать, что значительно позволит сэкономить время. Запуск тестов непосредственно после написания нового кода гарантирует сохранность ранее существовавших функций. Таким образом разработчик может быть уверенным в базе кода, особенно когда она внедряется в производственную среду, чтобы пользователи могли взаимодействовать с ней.

      Мы создаем примеры тестирования с помощью структур тестовых фреймворков. Mocha — это популярный тестовый фреймворк JavaScript, используемый для организации и запуска тестовых файлов. Однако Mocha не подтверждает поведение нашего кода. Для сравнения значений в тесте мы можем использовать модуль Node.js assert​​​​​​.

      В этой статье вы узнаете, как написать тесты для списка дел (TODO) для модуля Node.js. Для создания тестов будет настроен и использован фреймворк Mocha. Также будет использован модуль Node.js assert для создания самих тестов. В этом смысле вы будете использовать Mocha в качестве планировщика, а assert​​​ для реализации плана.

      Предварительные требования

      • Node.js, установленный на вашем компьютере для разработки. В этом обучающем руководстве используется версия Node.js 10.16.0. Чтобы установить его в macOS или Ubuntu 18.04, следуйте указаниям руководства Установка Node.js и создание локальной среды разработки в macOS или раздела Установка с помощью PPA руководства Установка Node.js в Ubuntu 18.04.
      • Базовые знания JavaScript, которые можно получить из нашей серии статей Программирование на JavaScript.

      Шаг 1 — Создание модуля Node

      Давайте начнем с написания модуля Node.js, который мы будем тестировать. Этот модуль будет управлять списком элементов TODO. Используя этот модуль, мы сможем перечислить все элементы списка TODO, которые нужно отследить, добавить новые элементы и отметить некоторые как выполненные. Также мы сможем экспортировать список элементов TODO в файл CSV. Если вам нужно вспомнить, как писать модули Node.js, прочтите нашу статью Создание модуля Node.js.

      Для начала необходимо настроить среду программирования. Создайте папку с именем проекта в своем терминале. В данном обучающем руководстве будет использоваться имя todos:

      Затем откройте эту папку:

      Теперь инициализируйте npm, поскольку позже мы будем использовать его функцию командной строки для запуска тестирования:

      У нас есть только одна зависимость, Mocha, которую мы будем использовать для организации и запуска тестов. Для загрузки и установки Mocha воспользуйтесь следующей командой:

      • npm i request --save-dev mocha

      Мы установим Mocha как зависимость dev, поскольку это не требуется модулем в производственных настройках. Если вы хотите узнать больше о пакетах Node.js или npm, ознакомьтесь с руководством Использование модулей Node.js с npm и package.json.

      Наконец, создадим файл, который будет содержать код нашего модуля:

      Теперь мы готовы создать наш модуль. Откройте index.js​​​ в текстовом редакторе, например nano:

      Давайте начнем с определения класса Todos. Этот класс содержит все функции, необходимые для управления нашим списком TODO. Добавьте следующие строки кода в index.js:

      todos/index.js

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

      Начнем с создания класса Todos. Его функция constructor() не принимает аргументов, поэтому нам не нужно предоставлять значения для создания объекта для данного класса. Все, что мы делаем, когда инициализируем объект Todos, — это создаем свойство todos, которое является пустым массивом.

      Линия модулей позволяет другим модулям Node.js требовать наш класс Todos. Без прямого экспорта класса тестовый файл, который мы создадим позже, не сможет использовать его.

      Давайте добавим функцию для возврата сохраненного массива todos. Запишите следующие выделенные строки:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      }
      
      module.exports = Todos;
      

      Функция list() возвращает копию массива, используемого классом. Она делает копию массива, используя деструктурирующий синтаксис JavaScript. Мы создаем копию массива, чтобы изменения, которые пользователь вносит в массив, возвращенный функцией list(), не влияли на массив, используемый объектом Todos.

      Примечание. Массивы JavaScript — это справочные файлы. Это значит, что для любого присваивания переменной для массива или вызова функции с массивом в качестве параметра JavaScript обращается к оригинальному созданному массиву. Например, если у нас есть массив с тремя элементами с именем x и мы создаем новую переменную y, так что y = x, y и x относятся к одному и тому же. Все изменения, выполняемые для массива с y, влияют на переменную x и наоборот.

      Теперь создадим функцию add(), которая добавляет новый элемент 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;
      

      Наша функция add() берет строку и помещает ее в свойство title нового объекта JavaScript. Новый объект также имеет свойство completed, которое по умолчанию устанавливается на false. Затем мы добавляем этот новый объект к нашему массиву TODO.

      Важной функцией в менеджере TODO является отметка элементов как завершенные. Для выполнения этой задачи мы пройдем в цикле по нашему массиву todos, чтобы найти элемент TODO, который ищет пользователь. Если элемент найден, отметим его как завершенный. Если ничего не найдено, выдадим ошибку.

      Добавьте функцию complete()​​​ следующим образом:

      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;
      

      Сохраните файл и выйдите из текстового редактора.

      Теперь у нас есть базовый менеджер TODO, с которым можно экспериментировать. Далее проверим код вручную, чтобы убедиться в работе приложения.

      Шаг 2 — Ручное тестирование кода

      В этом шаге мы запустим функции нашего кода и посмотрим на вывод, чтобы убедиться, что он соответствует ожиданиям. Это называется тестированием вручную. Оно выполняется аналогично наиболее распространенным методам тестирования, используемым программистами. Хотя позже мы автоматизируем тестирование с помощью Mocha, сначала протестируем наш код вручную, чтобы иметь лучшее представление о том, как тестирование вручную отличается от тестовых фреймворков.

      Добавим в наше приложение два элемента TODO и отметим один из них как завершенный. Запустите Node.js REPL в той же папке, что и файл index.js:

      Вы увидите командную строку > в REPL, которая указывает, что мы можем ввести код JavaScript. Введите в командную строку следующее:

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

      С помощью require() мы загружаем модуль TODO в переменную Todos. Помните, что наш модуль возвращает класс Todos по умолчанию.

      Теперь инстанцируем объект для этого класса. В REPL добавьте следующую строку кода:

      • const todos = new Todos();

      Мы можем использовать объект todos для проверки работы реализации. Добавим первый элемент TODO:

      До сих пор мы не видели никаких выводов в нашем терминале. Давайте убедимся, что мы сохранили элемент TODO run code, получив список всех наших TODO:

      Вы увидите следующий вывод в вашем REPL:

      Output

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

      Это ожидаемый результат: у нас есть один элемент TODO в нашем массиве TODO, и он не завершен по умолчанию.

      Добавим другой элемент TODO:

      • todos.add("test everything");

      Отметим первый элемент TODO как завершенный:

      • todos.complete("run code");

      Теперь наш объект todos будет управлять двумя элементами: run code и test everything. TODO run code также будет завершен. Подтвердим это, вызвав list()​​​ еще раз:

      Вывод REPL будет выглядеть следующим образом:

      Output

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

      Теперь закройте REPL следующим образом:

      Мы подтвердили, что наш модуль работает соответствующим образом. Хотя мы не поместили наш код в тестовый файл и не использовали тестовую библиотеку, мы вручную протестировали код. К сожалению, эта форма тестирования займет много времени, если ее использовать при выполнении каждого изменения. Далее попробуем выполнить автоматизированное тестирование в Node.js и посмотрим, возможно ли решить данную проблему с помощью тестового фреймворка Mocha.

      Шаг 3 — Создание первого теста с помощью Mocha и Assert

      В последнем шаге мы вручную протестировали наше приложение. Это будет работать в отдельных случаях, но по мере масштабирования модуля этот метод станет менее целесообразным. Поскольку тестируются новые функции, необходимо убедиться, что добавленная функциональность не создала проблем в предыдущем варианте. Мы хотели бы протестировать каждую функцию еще раз для каждого изменения в коде, но выполнение этой задачи вручную потребует огромных усилий и увеличит вероятность возникновения ошибок.

      Гораздо эффективнее настроить автоматическое тестирование. Тестирование по сценарию создается аналогично другим блокам кода. Мы запускаем наши функции с определенными вводами и проверяем их действие, чтобы убедиться, что они работают соответствующим образом. По мере роста базы кода мы будем автоматизировать тестирование. Когда мы прописываем тесты наряду с функциями, то можем проверить работоспособность всего модуля без необходимости каждый раз запоминать, как использовать ту или иную функцию.

      В этом обучающем руководстве мы используем тестовый фреймворк Mocha с модулем Node.js assert​​​. Давайте на практике посмотрим, как они вместе работают.

      Для начала создадим новый файл для хранения кода теста:

      Теперь с помощью предпочтительного текстового редактора откройте файл тестирования. Можно использовать nano, как раньше:

      В первой строке текстового файла мы загрузим модуль TODO аналогично тому, как мы делали в оболочке Node.js. Затем мы загрузим модуль assert​​​, чтобы он был на момент создания тестов. Добавьте следующие строки:

      todos/index.test.js

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

      Свойство strict​​​​ модуля assert позволит нам использовать специальные тесты эквивалентности, рекомендуемые Node.js, которые также подходят для проверок в дальнейшем, поскольку отвечают за большее число вариантов использования.

      Прежде чем приступить к написанию тестов, давайте обсудим, как Mocha организует наш код. Тестирование с использованием Mocha, как правило, использует следующие шаблоны:

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

      Обратите внимание на две ключевые функции: describe() и it()​​​. Функция describe() используется для группировки аналогичных тестов. Для Mocha не требуется запускать тесты, но их группировка упростит поддержку нашего кода теста. Рекомендуется группировать тесты таким образом, чтобы было проще обновлять аналогичные вместе.

      it() содержит наш код теста. Именно здесь мы могли бы взаимодействовать с функциями нашего модуля и использовать библиотеку assert​​. Многие функции it() могут быть определены в функции describe().

      Цель этого раздела состоит в использовании Mocha и assert для автоматизации нашего ручного теста. Мы будем делать это постепенно, начав с блока описания. Добавьте в файл следующее после строк модуля:

      todos/index.test.js

      ...
      describe("integration test", function() {
      });
      

      С помощью этого блока кода мы создали группировку для наших объединенных тестов. Тесты блока проверяют по одной функции за раз. Интеграционные тесты проверяют, насколько хорошо функции в модулях или между ними работают вместе. Когда Mocha запускает наш тест, все тесты в этом блоке описания будут запущены в группе интеграционных тестов.

      Давайте добавим функцию it(), чтобы начать тестирование нашего кода модуля:

      todos/index.test.js

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

      Обратите внимание, каким наглядным мы сделали название теста. Для всех, кто запустит наш тест, станет сразу понятно, что пройдено, а что — нет. Хорошо протестированное приложение — это, как правило, хорошо задокументированное приложение, и тесты иногда могут быть эффективным способом документирования.

      Для нашего первого теста мы создадим новый объект Todos и проверим, что в нем нет элементов:

      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);
          });
      });
      

      Первая новая строка кода инстанциировала новый объект Todos, как мы делали в Node.js REPL или другом модуле. Во второй новой строке мы использовали модуль assert​​​.

      Из модуля assert мы используем метод notStrictEqual()​​​. Эта функция учитывает два параметра: значение, которое необходимо протестировать (называется фактическое значение), и значение, которое мы ожидаем получить (называется ожидаемое значение). Если эти оба аргумента одинаковы, notStrictEqual()​​​ выдает ошибку о непрохождении теста.

      Сохраните и закройте index.test.js.

      Базовый сценарий будет истинным, так как длина должна быть 0, что не равно 1. Давайте убедимся в этом, запустив Mocha. Для этого нам потребуется модифицировать наш файл package.json. Откройте файл package.json в своем текстовом редакторе:

      Теперь в свойстве scripts измените его следующим образом:

      todos/package.json

      ...
      "scripts": {
          "test": "mocha index.test.js"
      },
      ...
      

      Мы только что изменили поведение команды test командной строки npm. Когда мы запустим npm test, npm проверит команду, которую мы только что ввели в package.json. Он будет искать библиотеку Mocha в нашей папке node_modules​​​ и запустит команду mocha с нашим файлом тестирования.

      Сохраните и закройте package.json.

      Давайте посмотрим, что происходит, когда мы запускаем наш тест. В своем терминале введите:

      Команда выдаст следующий вывод:

      Output

      > [email protected] test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

      Этот вывод сначала покажет нам, какая группа тестов сейчас запустится. Для каждого отдельного теста в группе тестовый сценарий является ступенчатым. Мы видим наше имя теста так, как мы описали его в функции it(). Галочка с левой стороны тестового сценария указывает на то, что тест пройден.

      Внизу мы получим резюме всех наших тестов. В нашем случае один тест был выполнен и завершен в течение 16 мс (время зависит от компьютера).

      Тестирование началось успешно. Однако текущий тестовый сценарий может допускать ложные позитивные результаты. Ложные позитивные результаты — это тестовый сценарий, когда тест пройден тогда, когда не должен.

      Теперь мы проверяем, что длина массива не равна 1. Давайте изменим тест, чтобы это условие было истинным, когда не должно. Добавьте следующие строки в 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);
          });
      });
      

      Сохраните и закройте файл.

      Мы добавили два элемента TODO. Давайте запустим тест, чтобы увидеть, что произойдет:

      В результате вы получите следующий вывод:

      Output

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

      Он проходит согласно ожиданиям, так как длина больше 1. Однако он не достигает первоначальной цели проведения этого первого теста. Первый тест должен был подтвердить, что мы начинаем с чистого состояния. Более совершенный тест подтвердит это во всех случаях.

      Давайте изменим тест таким образом, что его успешное прохождение будет возможным только при полном отсутствии 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();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Вы изменили notStrictEqual()​​​​​​ на strictEqual()​​​, функцию, которая проверяет эквивалентность между фактическим и ожидаемым аргументом. Строгое равенство (Strict equal) завершится неудачей, если наши аргументы не полностью одинаковы.

      Сохраните и закройте файл, затем запустите тест, чтобы увидеть, что произойдет:

      В этот раз вывод покажет ошибку:

      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.

      Этот текст пригодится только для отладки причины непрохождения теста. Обратите внимание, что поскольку тест не был пройден, в начале тестового сценария не было галочки.

      Резюме теста находится уже не внизу вывода, а сразу после нашего списка отображенных тестовых сценариев:

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

      В остальной части вывода предоставлены данные о непройденных тестах. Сначала мы видим, какие тестовые сценарии не пройдены:

      ...
      1) integrated 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)
      ...
      

      Выдается AssertionError, когда не выполняется strictEqual()​​​. Мы видим, что ожидаемое значение 0 отличается от фактического значения 2.

      Затем мы увидим строку в нашем файле тестирования, где код не выполняется. В этом случае это строка 10.

      Теперь мы воочию убедились, что наш тест не будет пройден, если мы будем ожидать некорректные результаты. Давайте изменим наш тестовый сценарий обратно на правильное значение. Откройте файл:

      Затем выберите строки todos.add, чтобы ваш код выглядел следующим образом:

      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);
          });
      });
      

      Сохраните и закройте файл.

      Запустите его еще раз, чтобы убедиться в прохождении без каких-либо ложных позитивных результатов:

      Вывод будет выглядеть следующим образом:

      Output

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

      Теперь мы значительно улучшили отказоустойчивость нашего теста. Давайте перейдем к нашему интеграционному тесту. Следующий шаг — добавить новый элемент 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}]);
          });
      });
      

      После использования функции add() мы подтверждаем, что у нас есть один элемент TODO, управляемый нашим объектом todos, при этом мы будем использовать strictEqual()​​. Наш следующий тест подтверждает данные в todos с помощью deepStrictEqual(). Функция deepStrictEqual() рекурсивно проверяет, имеют ли наши предполагаемые и реальные объекты одни и те же свойства. В этом случае проверяется, содержат ли оба ожидаемых массива объект JavaScript. Затем проверяется, имеют ли эти объекты JavaScript одинаковые свойства, т. е. оба их свойства title — это run code, а оба свойства completedfalse.

      Затем выполним оставшиеся тесты, используя эти два теста равенства, добавив следующие выделенные строки:

      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 }
                  ]
          );
        });
      });
      

      Сохраните и закройте файл.

      Теперь наш тест имитирует ручной тест. Благодаря этим программируемым тестам исчезает необходимость постоянной проверки выводов, если запускать тесты для контроля соответствия критериям. Обычно вы стараетесь проверить каждый шаг, чтобы убедиться в корректности тестирования кода.

      Давайте еще раз запустим тест npm test для получения данного вывода:

      Output

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

      Вы настроили комплексный тест с помощью Mocha и библиотеки assert.

      Рассмотрим ситуацию, когда мы разделили наш модуль с другими разработчиками, и теперь они предоставляют нам обратную связь. Большинство пользователей хотели бы, чтобы функция complete() возвращала ошибку, в случае если ни один элемент TODO еще не добавлен. Добавим это свойство в функцию complete().

      Откройте index.js в редакторе:

      Добавьте в функцию следующее:

      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}"`);
          }
      }
      ...
      

      Сохраните и закройте файл.

      Теперь добавим новый тест для этой новой функции. Нам нужно убедиться, что в случае если мы вызываем команду complete объекту Todos, в котором нет элементов, будет возвращена ошибка.

      Вернитесь в index.test.js​​:

      В конце файла добавьте следующий код:

      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);
          });
      });
      

      Снова используем describe() и it()​​. Этот тест начинается с создания нового объекта todos. Затем мы определяем ошибку, которую ожидаем получить при вызове функции complete().

      Далее используем функцию throws() модуля assert. Эта функция была создана для проверки ошибок, которые выдаются в коде. Первый аргумент — это функция, содержащая код, который выдает ошибку. Второй аргумент — это ошибка, которую мы ожидаем получить.

      Снова запустите тест с помощью npm test​​​ в своем терминале и вы увидите следующий вывод:

      Output

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

      Этот вывод подтверждает преимущества автоматизированного тестирования с помощью Mocha и assert. Поскольку наши тесты выполняются скриптами, каждый раз, когда мы запускаем npm test, мы проверяем, что все тесты успешно пройдены. Нам не нужно вручную проверять, работает ли другой код. Мы знаем, что работает, так как наш тест успешно пройден.

      Таким образом, с помощью этих тестов мы проверили результаты синхронного кода. Посмотрим, как можно адаптировать эти методы тестирования для работы с асинхронным кодом.

      Шаг 4 — Тестирование асинхронного кода

      Одна из функций, описанных в нашем модуле TODO, — это функция экспорта CSV. Она выводит все элементы TODO, а также завершенный статус в файл. Для этого требуется использовать модуль fs — встроенный модуль Node.js для работы с файловой системой.

      Запись в файл — это асинхронная операция. В Node.js есть много способов записи в файл. Можно использовать обратные вызовы, обещания или ключевые слова async/await. В этом разделе мы рассмотрим, как записывать тесты для разных методов.

      Обратные вызовы

      Функция callback — это функция, используемая как аргумент для асинхронной функции. Она вызывается при завершении асинхронной операции.

      Добавим функцию в наш класс Todos с именем saveToFile(). Эта функция будет создавать строку, проходя циклом через все элементы TODO и записывая эту строку в файл.

      Откройте файл index.js:

      Добавьте в этот файл следующий выделенный код:

      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,Completedn';
              this.todos.forEach((todo) => {
                  fileContents += `${todo.title},${todo.completed}n`
              });
      
              fs.writeFile('todos.csv', fileContents, callback);
          }
      }
      
      module.exports = Todos;
      

      Сначала необходимо импортировать модуль fs в наш файл. Затем добавляем новую функцию saveToFile()​​. Эта функция выполняет функцию обратного вызова, которая активируется сразу после завершения операции записи файла. В этой функции мы создаем переменную fileContents, содержащую всю строку, которую мы хотим сохранить в качестве файла. Она активируется с помощью заголовков CSV. Затем проходим циклом через каждый элемент TODO с помощью метода внутреннего массива forEach(). В процессе итерации добавляем свойства title и completed отдельных объектов todos.

      Наконец, используем модуль fs для записи файла с помощью функции writeFile(). Первый аргумент — это имя файла: todos.csv. Второй — это содержимое файла, в этом случае fileContents — это переменная. Последний аргумент — это наша функция обратного вызова, которая обрабатывает любые ошибки записи файла.

      Сохраните и закройте файл.

      Теперь напишем тест для функции saveToFile(). Этот тест выполняет две функции: в первую очередь подтверждает наличие файла, а затем проверяет, имеет ли файл правильное содержимое.

      Откройте файл index.test.js:

      Начнем с загрузки модуля fs в верхней части файла, так как мы будем использовать его для тестирования результатов:

      todos/index.test.js

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

      Теперь в конце файла добавим новый тест:

      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,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
                  done(err);
              });
          });
      });
      

      Как и ранее, используем команду describe() для группировки нашего теста отдельно от других, так как он подразумевает новую функцию. Функция it() несколько отличается от других функций. Обычно у используемой нами функции обратного вызова нет аргументов. В этот раз у нас есть done в качестве аргумента. Этот аргумент требуется при тестировании функций с обратными вызовами. Функция обратного вызова done() используется Mocha для информирования о завершении асинхронной функции.

      Все функции обратного вызова, протестированные в Mocha, должны вызывать обратный вызов done(). Если нет, Mocha не будет знать, когда функция была завершена, и зависнет в ожидании сигнала.

      Далее создаем экземпляр Todos и добавляем в него один элемент. Затем вызываем функцию saveToFile()​​​ с обратным вызовом, который фиксирует ошибку записи файла. Обратите внимание, как тест для этой функции располагается в обратном вызове. Если бы код теста был за пределами обратного вызова, тест бы не прошел, так как код вызывался до завершения записи файла.

      В нашей функции обратного вызова мы сначала проверяем наличие нашего файла:

      todos/index.test.js

      ...
      assert.strictEqual(fs.existsSync('todos.csv'), true);
      ...
      

      Функция fs.existsSync() возвращает true, если путь файла в аргументе существует, и false, если нет.

      Примечание. Функции модуля fs — асинхронные по умолчанию. Однако для ключевых функций существуют синхронные копии. Этот тест упрощен с помощью синхронных функций, так как нам не нужно встраивать асинхронный код для проверки работы теста. В модуле fs синхронные функции, как правило, имеют Sync ​​​в конце имен.

      Затем создаем переменную для хранения ожидаемого значения:

      todos/index.test.js

      ...
      let expectedFileContents = "Title,Completednsave a CSV,falsen";
      ...
      

      Используем readFileSync() модуля fs для синхронного чтения файла:

      todos/index.test.js

      ...
      let content = fs.readFileSync("todos.csv").toString();
      ...
      

      Теперь предоставляем readFileSync() правильный путь для файла: todos.csv​​. Поскольку readFileSync() возвращает буферный объект Buffer, который хранит бинарные данные, мы используем метод toString() для сравнения его значения со строкой, которую мы предположительно сохранили.

      Как и ранее, используем strictEqual модуля assert для выполнения сравнения:

      todos/index.test.js

      ...
      assert.strictEqual(content, expectedFileContents);
      ...
      

      Заканчиваем тест вызовом обратного вызова done()​​​, чтобы убедиться, что Mocha знает, что нужно остановить тестирование:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Мы указываем объект err в done(), тогда Mocha не пройдет тест в случае возникновения ошибки.

      Сохраните и закройте index.test.js.

      Запускаем этот тест с помощью npm test, как и ранее. Вы увидите следующий вывод на консоли:

      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)

      Вы протестировали первую асинхронную функцию с Mocha, используя функцию обратных вызовов. Но, как описывается в статье Написание асинхронного кода в Node.js, на момент написания этого обучающего руководства обещания используются чаще, чем обратные вызовы в новом коде Node.js. Далее давайте посмотрим, как протестировать их с помощью Mocha.

      Обещания

      Обещание — это объект JavaScript, который в конечном счете возвращает значение. Когда обещание успешно, оно разрешено. Когда встречается ошибка, оно отклоняется.

      Давайте изменим функцию saveToFile()​​ таким образом, чтобы она использовала обещания вместо обратных вызовов. Откройте index.js​​:

      Сначала нам нужно изменить загрузку модуля fs. В вашем файле index.js измените выражение require() в верхней части файла, чтобы это выглядело следующим образом:

      todos/index.js

      ...
      const fs = require('fs').promises;
      ...
      

      Мы только что импортировали модуль fs, который использует обещания, а не обратные вызовы. Теперь нам нужно внести некоторые изменения в команду saveToFile(), чтобы она работала с обещаниями.

      В вашем текстовом редакторе внесите в функцию saveToFile() следующие изменения для удаления обратных вызовов:

      todos/index.js

      ...
      saveToFile() {
          let fileContents = 'Title,Completedn';
          this.todos.forEach((todo) => {
              fileContents += `${todo.title},${todo.completed}n`
          });
      
          return fs.writeFile('todos.csv', fileContents);
      }
      ...
      

      Первое отличие — это тот факт, что наша функция больше не принимает никакие аргументы. В случае с обещаниями нам не нужна функция обратного вызова. Второе отличие касается того, как написан файл. Теперь мы возвращаем результат обещания writeFile().

      Сохраните и закройте index.js.

      Теперь давайте изменим наш тест так, чтобы он работал с обещаниями. Откройте index.test.js​​:

      Замените тест 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,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
              });
          });
      });
      

      Первое, что нужно изменить, — это удалить обратный вызов done() из аргументов. Если Mocha передает аргумент done(), его необходимо вызвать или он выдаст ошибку такого типа:

      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)
      

      При тестировании обещаний не включайте обратный вызов done() в it().

      Для проверки обещания нам нужно задать код утверждения в функцию then(). Обратите внимание, что мы возвращаем это обещание в тест, и у нас нет функции catch() для перехвата при отклонении обещания.

      Мы возвращаем обещание, чтобы любые ошибки, выданные в функции then(), всплыли в функции it(). Если ошибки не всплывают, Mocha не провалит тест. При тестировании обещаний вам нужно использовать return для тестируемого обещания. Если нет, вы рискуете получить ложный позитивный результат.

      Также мы пропускаем выражение catch(), так как Mocha может обнаружить, когда обещание отклоняется. При отклонении тест автоматически проваливается.

      Теперь, когда у нас есть тест, сохраните и закройте файл, затем запустите Mocha с npm test для подтверждения, что мы получим успешный результат:

      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)

      Мы изменили наш код и тест для использования обещаний, и теперь мы точно знаем, что это работает. Но последние асинхронные модели используют ключевые слова async/await, поэтому нам не нужно создавать множественные функции then() для обработки успешных результатов. Давайте посмотрим, как работает тест с async/await.

      async/await

      Ключевые слова async/await делают работу с обещаниями менее многословной. Когда мы определяем функцию как асинхронную с ключевым словом async, мы можем получить любые дальнейшие результаты в этой функции с ключевым словом await. Так мы можем использовать обещания без необходимости использования функций then() или catch().

      Можно упростить наш тест saveToFile(), который основан на обещании с async/await. В вашем текстовом редакторе создайте эти незначительные изменения к тесту saveToFile() в 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,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Первое изменение — это тот факт, что функция, используемая it(), имеет ключевое слово async, когда она определена. Это позволяет использовать ключевое слово await в ее теле.

      Второе изменение обнаруживается, когда мы вызываем saveToFile(). Ключевое слово await используется перед вызовом. Теперь Node.js знает, что нужно ждать, пока эта функция не решится перед продолжением теста.

      Код функции проще читать, когда мы переместили код, который был в функции then() в тело функции it(). Запуск этого кода с помощью npm test дает следующий вывод:

      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)

      Теперь мы можем тестировать асинхронные функции, используя любые три асинхронные парадигмы надлежащим образом.

      Мы охватили много вопросов о тестировании синхронного и асинхронного кода с Mocha. Далее переходим к другой функции, которую Mocha предлагает для улучшения нашего опыта тестирования, в частности в том, что касается использования хуков для изменения тестовых сред.

      Шаг 5 — Использование хуков для улучшения тестовых случаев

      Хуки — полезный элемент Mocha, который позволяет нам настроить среду до и после теста. Обычно мы добавляем хуки в блок функции describe(), так как они содержат логику установки и сноса, специфичную для некоторых тестовых случаев.

      Mocha предоставляет четыре типа хуков, которые используются в тестах:

      • before: этот хук запускается один раз перед началом первого теста.
      • beforeEach: этот хук запускается перед каждым тестовым случаем.
      • after: этот хук запускается один раз после завершения последнего тестового случая.
      • afterEach: этот хук запускается после каждого тестового случая.

      Когда мы тестируем функцию или свойство несколько раз, хуки очень помогают, так как они позволяют нам отделять код установки теста (например создание объекта todos) от кода утверждения теста.

      Для просмотра значения хуков добавим больше тестов в наш блок теста saveToFile().

      Хотя мы подтвердили, что можем сохранить элементы списка TODO в файл, мы сохранили только один элемент. Более того, элемент не был помечен как завершенный. Добавим больше тестов, чтобы убедиться, что различные аспекты нашего модуля работают.

      Сначала добавим второй тест для подтверждения того, что наш файл сохранен корректно, после завершения элемента списка TODO. Откройте файл 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,Completednsave a CSV,falsen";
              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,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Тест аналогичен тому, что мы делали ранее. Основное отличие в том, что мы вызываем функцию complete() перед вызовом saveToFile() и что ожидаемые элементы файла expectedFileContents​​​ теперь имеют значение true вместо false для столбца completed.

      Сохраните и закройте файл.

      Запустим наш новый тест, а также все остальные с помощью npm test​​​:

      В результате вы получите следующий вывод:

      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)

      Он работает, как и ожидалось. Но возможности для улучшения все еще есть. Они оба должны инстанцировать объект Todos в начале теста. По мере того как мы добавляем больше тестовых случаев, это быстро становится повторяющимся и отнимает память. Также каждый раз, когда мы запускаем тест, создается файл. Кто-то менее знакомый с модулем может ошибочно принять это за реальный вывод. Было бы неплохо очистить наши файлы вывода после тестирования.

      Сделаем эти улучшения с помощью тестовых хуков. Мы используем хук beforeEach() для настройки тестовой конфигурации элементов TODO. Тестовая конфигурация — это любое последовательное состояние, используемое в тесте. В нашем случае тестовая конфигурация — это новый объект todos, в который уже добавлен один элемент TODO. Затем мы используем afterEach() для удаления файла, созданного тестом.

      В index.test.js внесите следующие изменения в ваш последний тест для 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,Completednsave a CSV,falsen";
              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,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Давайте разберем все изменения, которые мы внесли. Мы добавили блок beforeEach() в тестовый блок:

      todos/index.test.js

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

      Эти две строки кода создают новый объект Todos, доступный для каждого нашего теста. С Mocha объект this в beforeEach() относится к такому же объекту this в it(). this одинаков для каждого блока кода внутри блока describe(). Подробнее о this ищите в нашем обучающем руководстве Понимание методов This, Bind, Call и Apply в JavaScript​​​.

      Благодаря мощному обмену контекстом мы можем быстро создавать тестовые конфигурации, которые подходят для обоих тестов.

      Затем мы очищаем наш файл CSV в функции afterEach():

      todos/index.test.js

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

      Если тест не прошел, то он не мог создать файл. Поэтому мы проверяем наличие файла перед использованием функции unlinkSync() для его удаления.

      Остальные изменения переключают ссылку с объектов todos, созданных ранее в функции it()​​​, на this.todos, имеющиеся в контексте Mocha. Также мы удалили строки, которые ранее инстанциировали todos в отдельных тестовых случаях.

      Теперь запустим этот файл, чтобы убедиться, что тест все еще работает. Введите в терминале npm test​​​, чтобы получить следующее:

      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)

      Результаты те же, и к тому же мы немного сократили время установки для новых тестов для функции saveToFile() и нашли решение для остаточного файла CSV.

      Заключение

      В этом обучающем руководстве мы написали модуль Node.js для управления элементами TODO и протестировали код вручную с помощью Node.js REPL. Затем создали тестовый файл и использовали фреймворк Mocha для запуска автоматизированных тестов. С помощью модуля assert мы смогли проверить корректность работы кода. Также протестировали синхронные и асинхронные функции с Mocha. Наконец, создали хуки с Mocha, которые делают написание связанных тестовых случаев более читабельным и управляемым.

      С помощью этих знаний попробуйте самостоятельно написать тесты для новых модулей Node.js, созданных вами. Вы можете подумать о вводах и выводах вашей функции и написать тест до написания кода?

      Если вы хотите узнать больше о тестовом фреймворке Mocha, ознакомьтесь с официальной документацией Mocha. Если вы хотите продолжить изучение Node.js, вы можете перейти к странице серии Написание кода на Node.js.



      Source link

      Como testar um módulo do Node.js com o Mocha e o assert


      O autor selecionou a Open Internet/Free Speech Fund para receber uma doação como parte do programa Write for DOnations.

      Introdução

      Realizar testes é uma parte fundamental no desenvolvimento de software. É comum que os programadores executem códigos que testam seu aplicativo, enquanto eles fazem alterações para confirmar se o aplicativo está se comportando como eles gostariam. Com a configuração de teste correta, este processo pode até ser automatizado, economizando bastante tempo. Realizar testes consistentemente após escrever um novo código garante que novas alterações não quebrem recursos pré-existentes. Isso dá ao desenvolvedor confiança em sua base de códigos, especialmente quando ela é implantada para a produção, para que os usuários possam interagir com ela.

      Um framework de teste estrutura a maneira como criamos casos de teste. O Mocha é um framework de teste popular em JavaScript. Ele organiza nossos casos de teste e executa-os para nós. No entanto, o Mocha não verifica o comportamento do nosso código. Para comparar valores em um teste, podemos utilizar o módulo assert do Node.js.

      Neste artigo, você escreverá testes para um módulo de lista TODO (de AFAZERES) do Node.js. Você configurará e usará o framework de teste Mocha para estruturar seus testes. Então, usará o módulo assert do Node.js para criar os testes de fato. Neste sentido, você usará o Mocha como um construtor de planos e o assert para implementar o plano.

      Pré-requisitos

      • Node.js instalado em sua máquina de desenvolvimento. Este tutorial utiliza a versão 10.16.0 do Node.js. Para instalar essa versão em macOS ou Ubuntu 18.04, siga os passos descritos no artigo sobre Como instalar o Node.js e criar um ambiente de desenvolvimento local em macOS ou a seção entitulada Instalando usando um PPA, do artigo sobre Como instalar o Node.js no Ubuntu 18.04.
      • Um conhecimento básico do JavaScript, que pode ser encontrado em nossa série Como programar em JavaScript.

      Passo 1 — Escrevendo um módulo do Node

      Vamos começar este artigo escrevendo o módulo do Node.js que queremos testar. Este módulo gerenciará itens de uma lista de AFAZERES. Usando este módulo, seremos capazes de listar todos os AFAZERES dos quais estamos mantendo o controle, adicionar novos itens e marcar alguns como completos. Além disso, vamos conseguir exportar uma lista de AFAZERES para um arquivo CSV. Caso queira um lembrete sobre módulos do Node.js, leia nosso artigo sobre Como criar um módulo do Node.js.

      Primeiro, precisamos configurar o ambiente de programação. Crie uma pasta com o nome do seu projeto no terminal. Este tutorial usará o nome todos:

      Então, acesse aquela pasta:

      Agora, inicialize o npm, pois vamos usar sua funcionalidade CLI para executar os testes mais tarde:

      Temos apenas uma dependência, o Mocha, que usaremos para organizar e executar nossos testes. Para baixar e instalar o Mocha, use o seguinte:

      • npm i request --save-dev mocha

      Instalamos o Mocha como uma dependência dev, pois o módulo não exige a presença dela em uma configuração de produção. Caso queira aprender mais sobre os pacotes do Node.js ou o npm, confira nosso guia sobre Como usar os módulos do Node.js com o npm e o package.json.

      Por fim, vamos criar o arquivo que terá o código do nosso módulo:

      Com isso, estamos prontos para criar nosso módulo. Abra o index.js em um editor de texto como o nano:

      Vamos começar definindo a classeTodos. Essa classe contém todas as funções que precisamos para gerenciar nossa lista de AFAZERES. Adicione as linhas de código a seguir ao index.js:

      todos/index.js

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

      Começamos o arquivo criando uma classe Todos. Sua função constructor() não recebe argumentos, de forma que não precisamos fornecer valores para instanciar objetos para esta classe. Tudo o que fazemos ao inicializar um objeto de Todos é criar uma propriedade de todos que é uma matriz vazia.

      A linha modules (módulos) permite que outros módulos do Node.js exijam nossa classe Todos. Sem exportar explicitamente a classe, o arquivo de teste que criaremos mais tarde não seria capaz de usá-la.

      Vamos adicionar uma função para retornar a matriz de todos que armazenamos. Escreva nas seguintes linhas em destaque:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      }
      
      module.exports = Todos;
      

      Nossa função list() retorna uma cópia da matriz que é usada pela classe. Ele faz uma cópia da matriz usando a sintaxe de desestruturação do JavaScript. Fazemos uma cópia da matriz, para que alterações que o usuário faz na matriz retornada por list() não afete a matriz usada pelo objeto Todos.

      Nota: as matrizes do JavaScript são tipos de referência. Isso significa que, para qualquer atribuição de variável a uma matriz ou invocação de função com uma matriz como parâmetro, o JavaScript se refere à matriz original criada. Por exemplo, caso tenhamos uma matriz com três itens chamados x e criemos uma nova variável, y, tal que y = x, y e x se referem à mesma coisa. Qualquer alteração que fizermos em y na matriz tem impacto na variável x e vice-versa.

      Agora, vamos escrever a função add(), que adiciona um novo item de AFAZERES:

      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;
      

      Nossa função add() recebe uma string e a coloca em uma nova propriedade de um objeto do JavaScript. O novo objeto também tem uma propriedade completed (finalizado), que é definida como false (falso) por padrão. Depois, adicionamos este novo objeto à nossa matriz de AFAZERES.

      Uma funcionalidade importante em um gestor de AFAZERES é marcar itens como finalizados. Para essa implantação, vamos fazer um loop que percorre nossa matriz todos para encontrar o item de AFAZERES que o usuário está procurando. Caso um seja encontrado, marcaremos o processo como concluído. Caso nenhum seja encontrado, emitiremos um erro.

      Adicione a função complete(), desta forma:

      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;
      

      Salve o arquivo e saia do editor de texto.

      Temos agora um gerenciador de AFAZERES básico, que podemos testar. Em seguida, vamos testar manualmente nosso código para ver se o aplicativo está funcionando.

      Passo 2 — Testando o código manualmente

      Neste passo, executaremos as funções do nosso código e observaremos o resultado para garantir que ele corresponda às nossas expectativas. Isso é chamado de teste manual. É provável que seja aplicada a metodologia de testes mais comum. Apesar do fato de que iremos automatizar nosso teste mais tarde com o Mocha, vamos primeiro testar manualmente nosso código para ter uma melhor ideia de como o teste varia entre diferentes frameworks de teste.

      Vamos adicionar dois itens de AFAZERES ao nosso aplicativo e marcar um deles como completo. Inicie o REPL do Node.js na mesma pasta que o arquivo index.js:

      Você verá o prompt > no REPL que nos diz que podemos inserir o código do JavaScript. Digite o seguinte no prompt:

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

      Com o require(), carregamos o módulo de AFAZERES em uma variável Todos. Lembre-se de que nosso módulo retorna a classe Todos por padrão.

      Agora, vamos instanciar um objeto para essa classe. No REPL, adicione esta linha de código:

      • const todos = new Todos();

      Podemos utilizar o objeto todos para verificar nossos trabalhos de implementação. Vamos adicionar nosso primeiro item de AFAZERES:

      Até agora, não vimos nenhum resultado em nosso terminal. Vamos verificar se armazenamos nosso item de AFAZERES "run code" (executar o código), obtendo uma lista de todos os nossos AFAZERES:

      Você verá este resultado em seu REPL:

      Output

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

      Este é o resultado esperado: temos um item de AFAZERES em nossa matriz de AFAZERES. Além disso, o arquivo não está concluído por padrão.

      Vamos adicionar outro item de AFAZERES:

      • todos.add("test everything");

      Marque o primeiro item de AFAZERES como concluído:

      • todos.complete("run code");

      Nosso objeto todos agora está gerenciando dois itens: "run code" e "test everything" (testar tudo). O item de AFAZERES "run code" também será concluído. Vamos confirmar isso, chamando list() novamente:

      O REPL irá gerar como resultado:

      Output

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

      Agora, saia do REPL com o seguinte:

      Confirmamos que nosso módulo se comporta da maneira como esperávamos. Embora não tenhamos colocado nosso código em um arquivo de teste ou usado uma biblioteca de teste, testamos nosso código manualmente. Infelizmente, essa forma de teste gasta muito tempo se feita todas as vezes que fizermos uma mudança. Em seguida, vamos utilizar testes automatizados no Node.js e ver se podemos resolver esse problema com o framework de testes Mocha.

      No último passo, testamos manualmente nosso aplicativo. Isso funciona para casos individuais de uso. Entretanto, conforme nosso módulo aumenta em escala, menos viável se torna esse método. À medida que testamos novas características, precisamos ter certeza de que adicionamos funcionalidades que não criaram problemas com funcionalidades já existentes. Seria interessante testarmos todas as funcionalidades para cada alteração no código mais uma vez. Porém, fazer isso manualmente necessitaria de muito esforço e estaria propenso a erros.

      Uma prática mais eficiente poderia ser configurar testes automatizados. Esses testes seguem scripts escritos como qualquer outro bloco de código. Executamos nossas funções com entradas definidas e inspecionamos seus efeitos para garantir que se comportam como esperamos. À medida que nossa base de código cresce, também crescem nossos testes automatizados. Ao escrevermos novos testes juntamente com as funcionalidades, podemos verificar se todos os módulos ainda funcionam — tudo isso sem precisar lembrar como usar cada função toda vez.

      Neste tutorial, estamos usando o framework de testes Mocha com o módulo assert do Node.js. Vamos colocar um pouco a mão na massa para ver como eles funcionam juntos.

      Para começar, crie um novo arquivo para armazenar nosso código de teste:

      Agora, utilize seu editor de texto preferido para abrir o arquivo de teste. Você pode usar o nano, assim como anteriormente:

      Na primeira linha do arquivo de texto, carregaremos o módulo de AFAZERES, assim como fizemos no shell do Node.js. Depois disso, carregaremos o módulo assert para quando escrevermos nossos testes. Adicione as linhas a seguir:

      todos/index.test.js

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

      A propriedade strict do módulo assert nos permitirá usar testes de igualdade especiais recomendados pelo Node.js. Esses testes são também adequados para testes futuros, já que representam mais casos de uso.

      Antes de escrevermos os testes, vamos discutir como o Mocha organiza o nosso código. Geralmente, os testes estruturados no Mocha seguem este modelo:

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

      Note duas funções chave: describe() e it(). A função describe() é usada para agrupar testes semelhantes. Ela não é exigida para que o Mocha realize testes, mas o agrupamento de testes torna nosso código de teste mais fácil de ser mantido. Recomenda-se que você agrupe seus testes de uma maneira que torne fácil a atualização dos que são semelhantes de uma só vez.

      O it() contém nosso código de teste. É aqui que interagimos com as funções do nosso módulo e usamos a biblioteca assert. Muitas funções it() podem ser definidas em uma função describe().

      Nosso objetivo nesta seção é usar o Mocha e o assert para automatizar nosso teste manual. Faremos isso passo a passo, começando com nosso bloco describe. Adicione isto ao seu arquivo após as linhas do módulo:

      todos/index.test.js

      ...
      describe("integration test", function() {
      });
      

      Com este bloco de código, criamos um agrupamento para nossos testes integrados. Os testes de unidade testariam uma função de cada vez. Os integration tests (testes de integração) verificam o quão bem as funções dentro de um módulo de diferentes módulos funcionam juntas. Quando o Mocha executa nosso teste, todos os testes dentro desse bloco describe serão executados no grupo "integration test".

      Vamos adicionar uma função it(), para que possamos começar a testar o código do nosso módulo:

      todos/index.test.js

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

      Note como escolhemos um nome descritivo para o teste. Caso alguém execute nosso teste, ficará imediatamente claro o que está passando ou falhando. Normalmente, um aplicativo bem testado é um aplicativo bem documentado. Além disso, os testes podem ser, por vezes, um tipo de documentação eficaz.

      Para nosso primeiro teste, criaremos um novo objeto Todos e verificaremos se ele não tem itens nele:

      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);
          });
      });
      

      A primeira linha de código instanciou um novo objeto Todos, assim como faríamos no REPL do Node.js ou outro módulo. Na segunda linha nova, usamos o módulo assert.

      A partir do módulo assert, usamos o método notStrictEqual(). Essa função recebe dois parâmetros: o valor que queremos testar (chamado de valor actual (real)) e o valor que esperamos obter (chamada de valor expected (esperado)). Caso ambos os argumentos sejam iguais, notStrictEqual() emite um erro para fazer o teste falhar.

      Salve e saia do index.test.js.

      O caso base será verdadeiro, pois o comprimento deveria ser 0, que é diferente de 1. Vamos confirmar isso executando o Mocha. Para fazer isso, precisamos modificar nosso arquivo package.json. Abra o seu arquivo package.json com seu editor de texto:

      Agora, modifique sua propriedade scripts para que se pareça com isto:

      todos/package.json

      ...
      "scripts": {
          "test": "mocha index.test.js"
      },
      ...
      

      Acabamos de alterar o comportamento do comando CLI test do npm. Ao executarmos npm test, o npm irá revisar o comando que acabamos de digitar no package.json. Ela irá procurar pela biblioteca do Mocha em nossa pasta node_modules e executará o comando mocha com nosso arquivo de teste.

      Salve e saia do package.json.

      Vamos ver o que acontece ao executarmos nosso teste. Em seu terminal, digite:

      O comando gerará o seguinte resultado:

      Output

      > [email protected] test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

      Em primeiro lugar, este resultado nos mostra qual grupo de testes está prestes a ser executado. Para cada teste dentro de um grupo, pula-se uma linha no caso de teste. Vemos nosso nome de teste da forma como o descrevemos na função it(). A marcação no lado esquerdo do caso de teste indica que o teste foi aprovado.

      No final, recebemos um resumo de todos os nossos testes. Em nosso caso, nosso único teste foi aprovado e foi concluído em 16ms (o tempo varia de computador para computador).

      Nossa testagem foi iniciada com sucesso. No entanto, este caso de teste atual pode permitir falsos positivos. Um falso positivo é um caso de teste que é aprovado quando na verdade deveria falhar.

      Neste momento, verificamos que o comprimento da matriz não é igual a 1. Vamos modificar o teste para que essa condição seja verdadeira quando não deveria. Adicione as linhas a seguir ao 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);
          });
      });
      

      Salve e saia do arquivo.

      Adicionamos dois itens de AFAZERES. Vamos executar o teste para ver o que acontece:

      Isso resultará no seguinte:

      Output

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

      O teste é aprovado conforme esperado, pois o comprimento é maior que 1. No entanto, ele anula o propósito original de ter aquele primeiro teste. O primeiro teste visa confirmar que estamos começando de um estado vazio. Um teste mais bem acabado confirmará isso, em todos os casos.

      Vamos alterar o teste, para que seja aprovado apenas se tivermos absolutamente nenhum item de AFAZERES em estoque. Faça as seguintes alterações no 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);
          });
      });
      

      Você alterou o notStrictEqual() para strictEqual(), uma função que verifica se há uma igualdade entre seu argumento real e esperado. A função strict equal falhará no caso de nossos argumentos não serem exatamente iguais.

      Salve, saia e então execute o teste para que possamos ver o que acontece:

      Desta vez, o resultado mostrará um erro:

      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.

      Este texto será útil para que possamos depurar o porquê do teste ter falhado. Note que, como o teste falhou, não houve marcador no início do caso de teste.

      Nosso resumo de teste já não está mais no final do resultado, mas sim logo após a exibição de nossa lista de casos de teste:

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

      O resultado que restou nos fornece dados sobre nossos testes que falharam. Primeiro, vemos qual caso de teste falhou:

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

      Em seguida, vemos o motivo pelo qual nosso teste falhou:

      ...
            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)
      ...
      

      Um AssertionError é lançado quando o strictEqual() falha. Vemos que o valor expected, 0, é diferente do valor actual, 2.

      Depois disso, vemos a linha do nosso arquivo de teste em que o código falha. Neste caso, é a linha 10.

      Agora, vimos por conta própria que nosso teste falhará se esperarmos valores incorretos. Vamos alterar nosso caso de teste novamente para o valor correto. Primeiro, abra o arquivo:

      Em seguida, retire as linhas todos.add para que seu código se pareça com o seguinte:

      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);
          });
      });
      

      Salve e saia do arquivo.

      Execute-o novamente para confirmar que ele foi aprovado sem possíveis falsos positivos:

      O resultado será o seguinte:

      Output

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

      Agora, melhoramos significativamente nossa resiliência do teste. Vamos prosseguir com o nosso teste de integração. O próximo passo é adicionar um novo item de AFAZERES no 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}]);
          });
      });
      

      Após usar a função add(), confirmamos que temos agora um item de AFAZERES sendo gerenciado pelo nosso objeto todos com o strictEqual(). Nosso próximo teste confirma os dados em todos com o deepStrictEqual(). A função deepStrictEqual() testa recursivamente se nossos objetos esperado e real possuem as mesmas propriedades. Neste caso, ela testa se ambas as matrizes esperadas possuem um objeto do JavaScript dentro delas. Depois disso, verifica se seus objetos do JavaScript possuem as mesmas propriedades, ou seja, se ambas as suas propriedades title são "run code" e ambas as suas propriedades completed são false.

      Depois disso, completamos os testes restantes usando esses dois controles de igualdade, conforme necessário, pela adição das seguintes linhas em destaque:

      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 }
                  ]
          );
        });
      });
      

      Salve e saia do arquivo.

      Nosso teste agora imita nosso teste manual. Com esses testes programáticos, não precisamos verificar o resultado continuamente se nossos testes forem aprovados quando os executarmos. Normalmente, deseja-se testar todos os aspectos de uso para garantir que o código seja testado corretamente.

      Vamos executar nosso teste com o npm test novamente para obter este resultado bastante conhecido:

      Output

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

      Agora, você configurou um teste integrado com o framework do Mocha e a biblioteca assert.

      Vamos considerar uma situação em que compartilhamos nosso módulo com outros desenvolvedores, e, agora, eles estão nos dando feedback. Uma boa parte de nossos usuários iria gostar que a função complete() retornasse um erro se nenhum item de AFAZERES tivesse sido adicionado até agora. Vamos adicionar essa funcionalidade em nossa função complete().

      Abra o index.js no seu editor de texto:

      Adicione o que vem a seguir à função:

      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}"`);
          }
      }
      ...
      

      Salve e saia do arquivo.

      Agora, vamos adicionar um novo teste para este novo recurso. Queremos verificar se, caso um objeto Todos que não possui itens seja chamado de completo, ele retornará nosso erro especial.

      Volte para o index.test.js:

      No final do arquivo, adicione o seguinte código:

      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);
          });
      });
      

      Usamos o describe() e o it() assim como anteriormente. Nosso teste começa com a criação de um novo objeto todos. Depois disso, definimos o erro que estamos esperando receber ao chamarmos a função complete().

      Em seguida, usamos a função throws() do módulo assert. Essa função foi criada para que possamos verificar os erros que são emitidos em nosso código. Seu primeiro argumento é uma função que contém o código que emite o erro. O segundo argumento é o erro que estamos esperando receber.

      Em seu terminal, execute novamente os testes com o npm test e você verá agora o seguinte resultado:

      Output

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

      Este resultado destaca o benefício e o porquê de realizarmos testes automatizados com o Mocha e o assert. Como nossos testes têm um script, toda vez que executamos o npm test, verificamos que todos os nossos testes estão sendo aprovados. Não precisamos verificar manualmente se o outro código ainda está funcionando; sabemos que está, porque o teste que fizemos ainda foi aprovado.

      Até agora, nossos testes verificaram os resultados de código síncrono. Vamos ver como precisaríamos adaptar nossos hábitos de teste recém-formados para trabalhar com código assíncrono.

      Passo 4 — Testando código assíncrono

      Uma das funcionalidades que queremos em nosso módulo de AFAZERES é um recurso de exportação em CSV. Isso imprimirá todos os AFAZERES que temos armazenados, junto com o status finalizado para um arquivo. Isso exige que usemos o módulo fs — um módulo integrado do Node.js para trabalhar com o sistema de arquivos.

      Escrever em um arquivo é uma operação assíncrona. Há várias maneiras de gravar em um arquivo no Node.js. Podemos usar callbacks, promessas, ou as palavras-chave async/await. Nesta seção, veremos como gravar testes para esses diferentes métodos.

      Callbacks

      Uma função callback é usada como um argumento para uma função assíncrona. Ela é chamada quando a operação assíncrona é concluída.

      Vamos adicionar uma função à nossa classe Todos chamada saveToFile(). Essa função construirá uma string, percorrendo em loop todos os nossos itens de AFAZERES e gravando a string em um arquivo.

      Abra o seu arquivo index.js:

      Neste arquivo, adicione o código em destaque a seguir:

      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,Completedn';
              this.todos.forEach((todo) => {
                  fileContents += `${todo.title},${todo.completed}n`
              });
      
              fs.writeFile('todos.csv', fileContents, callback);
          }
      }
      
      module.exports = Todos;
      

      Primeiro, precisamos importar o módulo fs para o nosso arquivo. Depois disso, adicionamos nossa nova função saveToFile(). Nossa função recebe uma função de callback que será usada assim que a operação de gravação do arquivo for concluída. Nessa função, criamos uma variável fileContents que armazena toda a string que queremos que seja salva como um arquivo. Ela é inicializada com os cabeçalhos do CSV. Depois disso, percorremos em loop cada item de AFAZERES com o método forEach() da matriz interna. À medida que iteramos, adicionamos as propriedades title e completed dos objetos todos individuais.

      Por fim, usamos o módulo fs para gravar o arquivo com a função writeFile(). Nosso primeiro argumento é o nome do arquivo: todos.csv. O segundo é o conteúdo do arquivo, sendo neste caso, nossa variável fileContents. Nosso último argumento é a nossa função callback, que lida com quaisquer erros de gravação de arquivos.

      Salve e saia do arquivo.

      Vamos agora escrever um teste para a nossa função saveToFile(). Nosso teste fará duas coisas: confirmar se o arquivo existe em primeiro lugar e, em seguida, verificar se ele tem o conteúdo correto.

      Abra o arquivo index.test.js:

      Vamos começar carregando o módulo fs no topo do arquivo, pois o usaremos para ajudar a testar nossos resultados:

      todos/index.test.js

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

      Agora, no final do arquivo, vamos adicionar nosso novo caso de teste:

      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,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
                  done(err);
              });
          });
      });
      

      Assim como anteriormente, usamos o describe() para agrupar nosso teste separadamente dos outros, uma vez que ele envolve uma nova funcionalidade. A função it() é ligeiramente diferente de nossas outras. Normalmente, a função callback que usamos não tem argumentos. Desta vez, temos done como um argumento. Precisamos desse argumento ao testar as funções com callbacks. A função callback done() é usada pelo Mocha para dizer a ele quando uma função assíncrona é concluída.

      Todas as funções callback que estão sendo testadas no Mocha devem chamar o callback done() Caso contrário, o Mocha nunca saberia quando a função foi concluída e ficaria preso à espera de um sinal.

      Continuando, criamos nossa instância Todos e adicionamos um único item a ela. Em seguida, chamamos a função saveToFile() com um call que captura um erro de gravação de arquivos. Note como nosso teste para essa função reside no callback. Caso nosso código de teste estivesse fora do callback, ele falharia, enquanto o código fosse chamado antes da gravação do arquivo terminar.

      Em nossa função callback, verificamos primeiro se o nosso arquivo existe:

      todos/index.test.js

      ...
      assert.strictEqual(fs.existsSync('todos.csv'), true);
      ...
      

      A função fs.existsSync() retorna true caso o caminho do arquivo em seu argumento exista, false caso contrário.

      Nota: as funções do módulo fs são assíncronas por padrão. No entanto, para as funções chave, foram criadas contrapartes síncronas. Este teste é mais simples ao usar funções síncronas, pois não precisamos aninhar o código assíncrono para garantir que ele funcione. No módulo fs, as funções síncronas geralmente têm seus nomes terminados com "Sync".

      Depois disso, criamos uma variável para armazenar nosso valor esperado:

      todos/index.test.js

      ...
      let expectedFileContents = "Title,Completednsave a CSV,falsen";
      ...
      

      Usamos o readFileSync() do módulo fs para ler o arquivo de maneira síncrona:

      todos/index.test.js

      ...
      let content = fs.readFileSync("todos.csv").toString();
      ...
      

      Agora, provisionamos o readFileSync() com o caminho correto para o arquivo: todos.csv. À medida que o readFileSync() retorna um objeto Buffer, que armazena dados binários, usamos seu método toString() para que possamos comparar seu valor com a string que esperamos que tenha sido salva.

      Assim como anteriormente, usamos o strictEqual, do módulo assert, para fazer uma comparação:

      todos/index.test.js

      ...
      assert.strictEqual(content, expectedFileContents);
      ...
      

      Terminamos nosso teste chamando o callback done(), garantindo que o Mocha saiba quando parar de testar esse caso:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Fornecemos o objeto err ao done(), para que o Mocha possa reprovar o teste caso tenha ocorrido um erro.

      Salve e saia do index.test.js.

      Vamos executar este teste com o npm test, assim como anteriormente. Seu console exibirá este resultado:

      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)

      Agora, você testou sua primeira função assíncrona com o Mocha usando callbacks. Mas no momento em que este tutorial é escrito, as Promessas são mais prevalentes que callbacks em novos códigos do Node.js, como explicado em nosso artigo Como escrever um código assíncrono em Node.js. Em seguida, vamos aprender também a como testá-los com o Mocha.

      Promessas

      Uma Promessa é um objeto do JavaScript que retornará, eventualmente, um valor. Quando uma Promessa é bem-sucedida, ela é resolvida. Quando encontra um erro, ela é rejeitada.

      Vamos modificar a função saveToFile(), para que ela utilize Promessas, em vez de callbacks. Abra o index.js:

      Primeiro, precisamos alterar a forma como o módulo fs é carregado. No seu arquivo index.js, modifique a declaração require() no topo do arquivo, para que se pareça com isto:

      todos/index.js

      ...
      const fs = require('fs').promises;
      ...
      

      Acabamos de importar o módulo fs que utiliza Promessas, em vez de callbacks. Agora, precisamos fazer algumas alterações no saveToFile(), para que ele funcione com Promessas.

      No seu editor de texto, faça as seguintes alterações à função saveToFile() para remover os callbacks:

      todos/index.js

      ...
      saveToFile() {
          let fileContents = 'Title,Completedn';
          this.todos.forEach((todo) => {
              fileContents += `${todo.title},${todo.completed}n`
          });
      
          return fs.writeFile('todos.csv', fileContents);
      }
      ...
      

      A primeira diferença é que nossa função já não aceita qualquer argumento. Com Promessas, não precisamos de uma função callback. A segunda mudança diz respeito à forma como o arquivo é gravado. Agora, retornamos o resultado da promessa writeFile().

      Salve e feche o index.js.

      Vamos agora adaptar nosso teste, para que ele funcione com Promessas. Abra o index.test.js:

      Altere o teste saveToFile(), substituindo por isto:

      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,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
              });
          });
      });
      

      A primeira mudança que precisamos fazer é remover o callback done() dos seus argumentos. Caso o Mocha passe o argumento done(), ele precisa ser chamado, ou emitirá um erro assim:

      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)
      

      Ao testar Promessas, não inclua o callback done() no it().

      Para testar nossa promessa, precisamos colocar nosso código de asserção na função then(). Note que retornamos essa promessa no teste e que não temos uma função catch() para capturar quando a Promessa é rejeitada.

      Retornaremos a promessa, para que quaisquer erros que forem lançados na função then() sejam borbulhados até a função it(). Caso os erros não sejam borbulhados, o Mocha não reprovará o caso de teste. Ao testar Promessas, é necessário usar o return na Promessa que está sendo testada. Caso contrário, existe o risco de se obter um falso positivo.

      Também omitimos a cláusula catch(), porque o Mocha pode detectar quando uma promessa é rejeitada. Caso tenha sido rejeitada, ele reprova automaticamente o teste.

      Agora que temos nosso teste funcionando, salve e saia do arquivo. Em seguida, execute o Mocha com o npm test para confirmar se recebemos um resultado bem-sucedido:

      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)

      Alteramos nosso código e teste para usar Promessas e agora sabemos que ele funciona. No entanto, os padrões assíncronos mais recentes utilizam as palavras-chave async/await. Isso é feito para que não precisemos criar várias funções then() para lidar com resultados bem-sucedidos. Vamos ver como testar com async/await.

      async/await

      As palavras-chave async/await tornam o trabalho com as Promessas menos prolixo. Assim que definimos uma função como assíncrona com a palavra-chave async, podemos obter quaisquer resultados futuros nessa função com a palavra-chave await. Desta maneira, podemos usar as Promessas sem precisar usar as funções then() ou catch().

      Podemos simplificar nosso teste saveToFile(), que é baseado em promessas, com async/await. No seu editor de texto, faça essas pequenas edições no teste saveToFile(), dentro de 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,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      A primeira mudança é que a função utilizada pela função it() agora tem a palavra-chave async quando é definida. Isso nos permite usar a palavra-chave await dentro do seu corpo.

      A segunda mudança é encontrada quando chamamos saveToFile(). A palavra-chave await é usada antes de ser chamada. Agora, o Node.js sabe que precisa esperar até que essa função seja resolvida antes de continuar o teste.

      Agora, o código da nossa função é mais fácil de se ler, uma vez que mudamos o código que estava na função then() para o corpo da função it(). Executar este código com o npm test produz este resultado:

      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)

      Agora, podemos testar as funções assíncronas usando qualquer um dos três paradigmas assíncronos adequadamente.

      Abordamos bastante conteúdo sobre realizar testes com o código síncrono e assíncrono com o Mocha. Em seguida, vamos ir um pouco mais fundo em algumas outras funcionalidades que o Mocha oferece para melhorar nossa experiência de teste. Em particular, como os ganchos podem mudar os ambientes de teste.

      Passo 5 — Usando hooks (ganchos) para melhorar os casos de teste

      Os ganchos são uma funcionalidade útil do Mocha que nos permite configurar o ambiente antes e após um teste. Normalmente, adicionamos ganchos dentro de um bloco de função describe(), já que eles possuem lógica de configuração e desmontagem específicos para alguns casos de teste.

      O Mocha provisiona quatro ganchos que podemos usar em nossos testes:

      • before: esse gancho é executado uma vez antes do início do teste.
      • beforeEach: este gancho é executado antes de todos os casos de teste.
      • after: este gancho é executado uma vez após o último caso de teste é concluído.
      • afterEach: este gancho é executado após todos os casos de teste.

      Quando testamos uma função ou recurso várias vezes, os ganchos são úteis, uma vez que nos permitem separar o código de configuração do teste (assim como na criação do objeto todos) do código de declaração do teste.

      Para ver o valor dos ganchos, vamos adicionar mais testes ao nosso bloco de teste saveToFile().

      Apesar de termos confirmado que podemos salvar nossos itens de AFAZERES em um arquivo, salvamos apenas um item. Além disso, o item não foi marcado como concluído. Vamos adicionar mais testes para ter certeza de que os vários aspectos do nosso módulo funcionam.

      Primeiro, vamos adicionar um segundo teste para confirmar que nosso arquivo é salvo corretamente quando concluímos um item de AFAZERES. Abra seu arquivo index.test.js no editor de texto:

      Troque o último teste pelo seguinte:

      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,Completednsave a CSV,falsen";
              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,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      O teste é semelhante ao que tínhamos anteriormente. As principais diferenças são que chamamos a função complete() antes de chamar saveToFile(), e que nosso expectedFileContents tem agora agora true, ao invés de false para o valor completed da coluna.

      Salve e saia do arquivo.

      Vamos executar nosso novo teste, e todos os outros, com o npm test:

      Isso resultará no seguinte:

      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)

      Ele está funcionando conforme esperado. No entanto, há espaço para melhorias. Ambos precisam instanciar um objeto Todos no início do teste. À medida que adicionamos mais casos de teste, isso torna-se rapidamente repetitivo e um desperdício de memória. Além disso, sempre que executamos o teste, ele cria um arquivo. Isso pode ser confundido com o resultado real por alguém menos familiarizado com o módulo. Seria bom se limpássemos nossos arquivos do resultado após os testes.

      Vamos fazer essas melhorias usando ganchos de teste. Vamos usar o gancho beforeEach() para configurar nosso acessório de teste dos itens de AFAZERES. Um acessório de teste é qualquer estado consistente usado em um teste. Em nosso caso, nosso acessório de teste é um novo objeto todos que tem um item de AFAZERES já adicionado nele. Depois disso, usaremos o afterEach() para remover o arquivo criado pelo teste.

      No index.test.js, faça as seguintes alterações no seu último teste em 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,Completednsave a CSV,falsen";
              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,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Vamos detalhar todas as alterações que fizemos. Adicionamos um bloco de beforeEach() no bloco de teste:

      todos/index.test.js

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

      Essas duas linhas de código criam um novo objeto Todos que ficará disponível em cada um dos nossos testes. Com o Mocha, o objeto this em beforeEach() refere-se ao mesmo objeto this no it(). O this é o mesmo para todos os blocos de código dentro do bloco describe(). Para obter mais informações sobre o this, consulte nosso tutorial Entendendo this, bind, call e apply no JavaScript.

      Este poderoso compartilhamento de contexto é o motivo pelo qual podemos criar rapidamente os acessórios de teste que funcionam para ambos os nossos testes.

      Depois disso, limparemos nosso arquivo CSV na função afterEach():

      todos/index.test.js

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

      Caso nosso teste tenha falhado, então ele pode não ter criado um arquivo. Por esse motivo, verificamos se o arquivo existe antes de usar a função unlinkSync() para excluí-lo.

      As alterações remanescentes trocam a referência dos todos, que foram criados anteriormente na função it(), para this.todos, que está disponível no contexto do Mocha. Também excluímos as linhas que instanciaram os todos anteriormente nos casos de teste individuais.

      Agora, vamos executar este arquivo para confirmar se nossos testes ainda funcionam. Insira o npm teste em seu terminal para obter:

      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)

      Os resultados são os mesmos e, como um benefício, reduzimos ligeiramente o tempo de configuração dos novos testes para a função saveToFile(). Além disso, encontramos uma solução para o arquivo CSV residual.

      Conclusão

      Neste tutorial, você escreveu um módulo do Node.js para gerenciar os itens de AFAZERES e testou o código manualmente usando o REPL do Node.js. Depois disso, criou um arquivo de teste e usou o framework do Mocha para executar testes automatizados. Com o módulo assert, você foi capaz de verificar se seu código funciona. Você também testou funções síncronas e assíncronas com o Mocha. Por fim, criou ganchos com o Mocha que fazem com que escrever vários casos de teste relacionados seja muito mais legível e sustentável.

      Equipado com este conhecimento, desafio você a escrever testes para novos módulos do Node.js que está criando. Consegue pensar nas entradas e resultados da sua função e escrever seu teste antes de escrever seu código?

      Caso queira mais informações sobre o framework de teste do Mocha, confira a documentação oficial do Mocha. Caso queira continuar aprendendo sobre o Node.js, volte para a página da série Como codificar em Node.js.



      Source link