One place for hosting & domains

      So testen Sie ein Node.js-Modul mit Mocha und Assert


      Die Autorin wählte den Open Internet/Free Speech Fund, um eine Spende im Rahmen des Programms Write for DOnations zu erhalten.

      Einführung

      Testen ist ein integraler Bestandteil der Softwareentwicklung. Es ist üblich, dass Programmierer Code ausführen, der ihre Anwendung testet, während sie darin Änderungen vornehmen. So können sie bestätigen, dass sich die Anwendung so verhält, wie sie es gerne hätten. Mit dem richtigen Test-Setup kann dieser Prozess sogar automatisiert sein und somit eine Menge Zeit sparen. Das Ausführen von Tests nach dem Schreiben von neuem Code stellt sicher, dass neue Änderungen keine bereits vorhandenen Funktionen brechen. Das gibt dem Entwickler Vertrauen in seine Code-Basis, insbesondere dann, wenn der Code produktiv eingesetzt wird, damit die Benutzer mit ihm interagieren können.

      Ein Test-Framework strukturiert die Art, wie wir Testfälle erstellen. Mocha ist ein beliebtes JavaScript-Framework, das unsere Testfälle organisiert und für uns ausführt. Mocha verifiziert jedoch nicht das Verhalten unseres Codes. Um Werte in einem Test zu vergleichen, können wir das Node.js-assert-Modul verwenden.

      In diesem Artikel schreiben Sie Tests für ein Node.js-TODO-Listenmodul. Sie richten das Testframework von Mocha ein und nutzen es, um Ihre Tests zu strukturieren. Dann verwenden Sie das Node.js-assert-Modul, um die Tests selbst zu erstellen. In diesem Sinne verwenden Sie Mocha als Planersteller und assert zur Umsetzung des Plans.

      Voraussetzungen

      Schritt 1 — Schreiben eines Node-Moduls

      Beginnen wir diesen Artikel mit dem Schreiben des Node.js-Moduls, das wir testen möchten. Dieses Modul verwaltet eine Liste von TODO-Elementen. Mithilfe dieses Moduls können wir alle TODOs auflisten, die wir verfolgen, sowie neue Elemente hinzufügen und einige als abgeschlossen markieren. Zusätzlich können wir eine Liste von TODO-Elementen in eine CSV-Datei exportieren. Wenn Sie eine Auffrischung über das Schreiben von Node.js-Modulen wünschen, können Sie unseren Artikel Erstellen eines Node.js-Moduls lesen.

      Zuerst müssen wir die Codierungsumgebung einrichten. Erstellen Sie einen Ordner mit dem Namen Ihres Projekts in Ihrem Terminal. Dieses Tutorial verwendet den Namen todos:

      Gehen Sie dann in diesen Ordner hinein:

      Initialisieren Sie nun npm, da wir später zum Ausführen der Tests seine CLI-Funktionalität verwenden:

      Wir haben nur eine Abhängigkeit, Mocha, das wir zur Organisation und Durchführung unserer Tests verwenden. Zum Herunterladen und Installieren von Mocha verwenden Sie Folgendes:

      • npm i request --save-dev mocha

      Wir installieren Mocha als dev-Abhängigkeit, da es für das Modul in einer Produktionsumgebung nicht erforderlich ist. Wenn Sie mehr über Node.js-Packages oder npm erfahren möchten, lesen Sie den Leitfaden Verwenden von Node.js-Modulen mit npm und package.json.

      Abschließend erstellen wir unsere Datei, die den Code unseres Moduls enthalten wird:

      Damit können wir nun unser Modul erstellen. Öffnen Sie index.js in einem Texteditor wie nano:

      Zuerst definieren wir die Todos-Klasse. Diese Klasse enthält alle Funktionen, die wir zur Verwaltung unserer TODO-Liste benötigen. Fügen Sie index.js die folgenden Zeilen von Code hinzu:

      todos/index.js

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

      Wir beginnen mit der Datei, indem wir eine Todos-Klasse erstellen. Seine Funktion constructor() nimmt keine Argumente an, daher müssen wir keine Werte bereitstellen, um ein Objekt für diese Klasse zu instanziieren. Wenn wir ein Todos-Objekt initialisieren, erstellen wir lediglich eine todos-Funktion, bei der es sich um ein leeres Array handelt.

      Die modules-Zeile ermöglicht es anderen Node.js-Modulen, unsere Todos-Klasse zu verlangen. Wenn wir sie nicht ausdrücklich exportieren, könnte die Testdatei, die wir später erstellen, sie nicht verwenden.

      Wir fügen nun eine Funktion hinzu, um das Array von todos, das wir gespeichert haben, auszugeben. Fügen Sie die folgenden hervorgehobenen Zeilen ein:

      todos/index.js

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

      Unsere list()-Funktion gibt eine Kopie des Arrays aus, die von der Klasse verwendet wird. Sie erstellt die Kopie des Arrays mit der destrukturierten Syntax von JavaScript. Wir erstellen eine Kopie des Arrays, damit die Änderungen, die der Benutzer an dem von list() ausgegebenen Array vornimmt, nicht das vom Todos-Objekt verwendete Array beeinträchtigen.

      Anmerkung: JavaScript-Arrays sind Referenztypen. Das bedeutet, dass sich JavaScript bei jeder Variablenzuweisung an ein Array oder bei Funktionsaufrufen mit einem Array als Parameter auf das ursprüngliche Array bezieht, das erstellt wurde. Wenn wir zum Beispiel ein Array mit drei Elementen namens x haben und eine neue Variable y erstellen, sodass y = x, y und x sich beide auf dieselbe Sache beziehen. Alle Änderungen, die wir im Array an y vornehmen, wirken sich auf die Variable x aus und umgekehrt.

      Schreiben wir nun die add()-Funktion, die ein neues TODO-Element hinzufügt:

      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;
      

      Unsere add()-Funktion nimmt eine Zeichenfolge und platziert sie bei einem neuen JavaScript-Object in die title-Funktion. Das neue Objekt hat auch eine completed-Funktion, die standardmäßig auf false gesetzt ist. Dann fügen wir unser neues Objekt unserem Array von TODOs hinzu.

      Eine wichtige Funktionalität in einem TODO-Manager ist die Markierung von Elementen als abgeschlossen. Um dies umzusetzen, durchlaufen wir unser todos-Array, um das TODO-Element zu finden, nach dem der Benutzer sucht. Wenn eines gefunden wird, markieren wir es als abgeschlossen. Wenn keines gefunden wird, geben wir einen Fehler aus.

      Fügen Sie die complete()-Funktion wie hier gezeigt hinzu:

      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;
      

      Speichern Sie die Datei und beenden Sie den Texteditor.

      Wir verfügen nun über einen grundlegenden TODO-Manager, mit dem wir experimentieren können. Als Nächstes testen wir den Code manuell, um zu sehen, ob die Anwendung funktioniert.

      Schritt 2 — Manuelles Testen des Codes

      In diesem Schritt führen wir die Funktionen unseres Codes aus und beobachten die Ausgabe, um sicherzustellen, dass sie unseren Erwartungen entspricht. Das nennt sich manuelles Testen. Es ist wahrscheinlich die gebräuchlichste Testmethodik, die Programmierer anwenden. Obwohl wir unsere Tests später mit Mocha automatisieren, testen wir unseren Code zunächst manuell, um ein besseres Gefühl dafür zu bekommen, wie sich manuelles Testen von Test-Frameworks unterscheidet.

      Wir fügen unserer Anwendung zwei TODO-Elemente hinzu und markieren eines als abgeschlossen. Starten Sie die Node.js REPL in dem gleichen Ordner wie die Datei index.js:

      Sie sehen die Eingabeaufforderung > in der REPL, die uns anzeigt, dass wir JavaScript-Code eingeben können. Geben Sie Folgendes an der Eingabeaufforderung ein:

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

      Mit require() laden wir das TODOs-Modul in eine Todos-Variable. Erinnern Sie sich, dass unser Modul die Todos-Klasse standardmäßig ausgibt.

      Lassen Sie uns nun ein Objekt für diese Klasse instanziieren. Fügen Sie diese Zeile von Code in der REPL hinzu:

      • const todos = new Todos();

      Wir können das todos-Objekt verwenden, um unsere Umsetzungsarbeiten zu verifizieren. Fügen wir unser erstes TODO-Element hinzu:

      Bisher haben wir noch keine Ausgabe in unserem Terminal gesehen. Verifizieren wir, dass wir unser "run code"-TODO-Element gespeichert haben, indem wir eine Liste aller unserer TODOs abrufen:

      Sie sehen diese Ausgabe in Ihrer REPL:

      Output

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

      Das ist das erwartete Ergebnis: Wir haben ein TODO-Element in unserem Array von TODOs und es ist nicht standardmäßig abgeschlossen.

      Wir fügen ein weiteres TODO-Element hinzu:

      • todos.add("test everything");

      Markieren Sie das erste TODO-Element als abgeschlossen:

      • todos.complete("run code");

      Unser todos-Objekt verwaltet nun zwei Elemente: "run code" und "test everything". Das "run code"-TODO wird ebenfalls abgeschlossen sein. Bestätigen wir dies, indem wir list() erneut aufrufen:

      Die REPL wird Folgendes ausgeben:

      Output

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

      Beenden Sie nun die REPL wie folgt:

      Wir haben bestätigt, dass sich unser Modul erwartungsgemäß verhält. Wir haben unseren Code nicht in eine Testdatei gestellt oder eine Testbibliothek verwendet, sondern manuell getestet. Leider ist diese Testform zeitaufwendig, wenn wir sie bei jeder Änderung, die wir vornehmen, durchführen. Als Nächstes verwenden wir automatisiertes Testen in Node.js und sehen, ob wir dieses Problem mit dem Mocha lösen können.

      Schritt 3 — Schreiben des ersten Tests mit Mocha und Assert

      Im letzten Schritt haben wir unsere Anwendung manuell getestet. Das funktioniert für individuelle Anwendungsfälle, aber wenn unser Modul skaliert, wird diese Methode weniger praktikabel. Wenn wir neue Eigenschaften testen, müssen wir sicher sein, dass die hinzugefügte Funktionalität keine Probleme in der alten Funktionalität verursacht. Wir möchten jede Eigenschaft bei jeder Änderung des Codes erneut testen, aber dies von Hand zu tun wäre sehr aufwendig und fehleranfällig.

      Eine effizientere Praxis wäre die Einrichtung automatisierter Tests. Das sind skriptbasierte Tests, die wie jeder andere Codeblock geschrieben sind. Wir führen unsere Funktionen mit definierten Eingaben aus und inspizieren ihre Effekte, um sicherzustellen, dass sie sich wie erwartet verhalten. Mit dem Anwachsen unserer Codebasis wächst auch der Umfang der automatisierten Tests. Wenn wir neue Tests zusammen mit den Eigenschaften schreiben, können wir überprüfen, ob das gesamte Modul noch funktioniert – ohne sich jedesmal daran erinnern zu müssen, wie jede einzelne Funktion genutzt wird.

      In diesem Tutorial verwenden wir das Testframework von Mocha mit dem Node.js-assert-Modul. Wir sammeln ein paar praktische Erfahrungen, um zu sehen, wie sie zusammenarbeiten.

      Erstellen Sie zunächst eine neue Datei, um unseren Testcode zu speichern:

      Verwenden Sie nun Ihren bevorzugten Texteditor, um die Testdatei zu öffnen. Sie können wie zuvor nano verwenden:

      In der ersten Zeile der Textdatei laden wir das TODOs-Modul, so wie wir es mit der Node.js-Shell getan haben. Dann laden wir das assert-Modul für das Schreiben unserer Tests. Fügen Sie die folgenden Zeilen hinzu:

      todos/index.test.js

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

      Die strict-Funktion des assert-Moduls erlaubt uns, spezielle Gleichheitstests anzuwenden, die von Node.js empfohlen werden und gut für das zukünftige Prüfen geeignet sind, da sie mehr Anwendungsfälle berücksichtigen.

      Bevor wir mit dem Schreiben von Tests beginnen, behandeln wir, wie Mocha unseren Code organisiert. Die in Mocha strukturierten Tests folgen normalerweise dieser Vorlage:

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

      Beachten Sie zwei Schlüsselfunktionen: describe() und it(). Die describe()-Funktion wird zur Gruppierung ähnlicher Tests genutzt. Es ist nicht erforderlich, für Mocha Tests auszuführen, aber die Gruppierung von Tests erleichtert die Pflege unseres Testcodes. Es wird empfohlen, Ihre Tests so zu gruppieren, dass Sie ähnliche Tests leicht zusammen aktualisieren können.

      Die Funktion it() enthält unseren Testcode. Hier würden wir mit den Funktionen unseres Moduls interagieren und die assert-Bibliothek verwenden. Viele it()-Funktionen können als describe()-Funktion definiert werden.

      Unser Ziel in diesem Abschnitt ist die Verwendung von Mocha und assert zur Automatisierung unserer manuellen Tests. Wir führen dies Schritt für Schritt aus und beginnen mit unserem Beschreibungsblock. Fügen Sie Folgendes nach den Modulzeilen in Ihre Datei ein:

      todos/index.test.js

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

      Mit diesem Codeblock haben wir eine Gruppierung für unsere integrierten Tests erstellt. Komponententests würden jeweils nur eine Funktion testen. Integrationstests verifizieren, wie gut Funktionen innerhalb oder über Module zusammenarbeiten. Wenn Mocha unseren Test ausführt, laufen alle Tests innerhalb des Beschreibungsblocks unter der "integration test"-Gruppe.

      Wir fügen nun eine it()-Funktion hinzu, damit wir mit dem Testen des Codes unseres Moduls beginnen können:

      todos/index.test.js

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

      Beachten Sie, wie deskriptiv wir den Namen des Tests gemacht haben. Wenn jemand unseren Test ausführt, wird sofort klar, was passiert oder fehlgeschlagen ist. Eine gut getestete Anwendung ist typischerweise eine gut dokumentierte Anwendung und Tests können manchmal eine effektive Art der Dokumentation sein.

      Für unseren ersten Test erstellen wir ein neues Todos-Objekt und verifizieren, dass es keine Elemente enthält:

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

      Die erste neue Zeile des Codes instanziierte ein neues Todos-Objekt, wie wir es in der Node.js-REPL oder einem anderen Modul tun würden. In der zweiten neuen Zeile verwenden wir das assert-Modul.

      Aus dem assert-Modul verwenden wir die notStrictEqual()-Methode. Diese Funktion nimmt zwei Parameter: den Wert, den wir testen möchten (genannt actual-Wert) und den Wert, den wir erhalten möchten (genannt expected-Wert). Wenn beide Argumente gleich sind, gibt notStrictEqual() einen Fehler aus, damit der Test fehlschlägt.

      Speichern und beenden Sie index.test.js.

      Der Basisfall wird wahr sein, da die Länge 0 sein sollte, was nicht 1 ist. Bestätigen wir das, indem wir Mocha ausführen. Dazu müssen wir unsere package.json-Datei ändern. Öffnen Sie Ihre package.json-Datein mit Ihrem Texteditor:

      Ändern Sie diese nun in der scripts-Funktion so wie hier gezeigt:

      todos/package.json

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

      Wir haben nun das Verhalten des npm-CLI-Befehls test geändert. Wenn wir npm test ausführen, überprüft npm den gerade eingegebenen Befehl in package.json. Die Ausführung sucht nach der Mocha-Bibliothek in unserem node_modules-Ordner und führt den mocha-Befehl mit unserer Testdatei aus.

      Speichern und beenden Sie package.json.

      Nun sehen wir uns an, was passiert, wenn wir unseren Test ausführen. Geben Sie Folgendes in Ihr Terminal ein:

      Der Befehl erzeugt die folgende Ausgabe:

      Output

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

      Diese Ausgabe zeigt uns zunächst, welche Testgruppe sie nun ausführen wird. Für jeden einzelnen Test innerhalb einer Gruppe ist der Testfall einbezogen. Wir sehen unseren Testnamen wie in der Funktion it() beschrieben. Das Häkchen auf der linken Seite des Testfalls zeigt an, dass der Test bestanden ist.

      Am Ende erhalten wir eine Zusammenfassung aller Tests. In unserem Fall ist unser einzelner Test bestanden und wurde in 16 ms abgeschlossen (die Zeit variiert von Computer zu Computer).

      Unsere Testung hat erfolgreich begonnen. Der aktuelle Testfall kann jedoch falsch-positive Meldungen liefern. Ein falsch-positiver Testfall ist ein Testfall, der bestanden wird, wenn er fehlschlagen sollte.

      Wir überprüfen gerade, dass die Länge des Arrays nicht gleich 1 ist. Wir werden den Test nun so ändern, dass dieser Zustand auch dann zutrifft, wenn er es nicht sollte. Fügen Sie der index.test.js folgende Zeilen hinzu:

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

      Speichern und schließen Sie die Datei.

      Wir haben zwei TODO-Elemente hinzugefügt. Wir führen nun den Test aus, um zu sehen, was passiert:

      Dadurch ergibt sich Folgendes:

      Output

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

      Wie ewartet besteht der Test, da die Länge größer als 1 ist. Jedoch wird der ursprüngliche Zweck des ersten Tests verfehlt. Der erste Test ist dazu gedacht, zu bestätigen, dass wir mit einem Leerzustand beginnen. Ein besserer Test bestätigt dies in allen Fällen.

      Wir ändern nun den Test, damit er nur dann bestanden wird, wenn wir absolut keine TODOs im Speicher haben. Führen Sie die folgenden Änderungen in der index.test.js aus:

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

      Sie haben notStrictEqual() auf strictEqual() geändert, eine Funktion, die die Gleichheit zwischen dem tatsächlichen und erwarteten Argument überprüft. Die Strict-Equal-Funktion schlägt fehl, wenn unsere Argumente nicht genau gleich sind.

      Speichern und beenden Sie und führen Sie dann den Test aus, damit wir sehen können, was passiert:

      Dieses Mal zeigt die Ausgabe einen Fehler:

      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.

      Dieser Text wird uns helfen, herauszufinden, warum der Test fehlgeschlagen ist. Beachten Sie, dass zu Beginn des Testfalls kein Häkchen vorhanden ist, da der Test fehlgeschlagen ist.

      Unsere Testzusammenfassung befindet sich nicht mehr am Ende der Ausgabe, sondern direkt nach der Anzeige unserer Liste von Testfällen:

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

      Die verbleibende Ausgabe gibt uns Daten über unsere fehlgeschlagenen Tests. Zuerst sehen wir, welcher Testfall fehlgeschlagen ist:

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

      Dann sehen wir, warum unser Test fehlgeschlagen ist:

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

      Es wird ein AssertionError gemeldet, wenn strictEqual() fehlschlägt. Wir sehen, dass der expected-Wert, 0, vom actual-Wert, 2, abweicht.

      Dann sehen wir die Zeile in unserer Testdatei, in der der Code fehlschlägt. In diesem Fall ist es Zeile 10.

      Nun haben wir selbst gesehen, dass unser Test fehlschlägt, wenn wir fehlerhafte Werte erwarten. Wir ändern unseren Testfall wieder auf seinen richtigen Wert. Öffnen Sie die Datei:

      Dann nehmen Sie die todos.add-Zeilen heraus, sodass Ihr Code wie folgt aussieht:

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

      Speichern und schließen Sie die Datei.

      Führen Sie ihn erneut aus, um zu bestätigen, dass er ohne potenzielle falsch-positive Meldungen besteht:

      Sie erhalten folgende Ausgabe:

      Output

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

      Wir haben nun die Belastbarkeit unseres Tests deutlich verbessert. Fahren wir mit unserem Integrationstest fort. Der nächste Schritt ist das Hinzufügen eines neuen TODO-Elements in 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}]);
          });
      });
      

      Nach der Verwendung der add()-Funktion bestätigen wir, dass wir nun ein TODO haben, das von unserem todos-Objekt mit strictEqual() verwaltet wird. Unser nächster Test bestätigt die Daten in den todos mit deepStrictEqual(). Die Funktion deepStrictEqual() prüft rekursiv, ob unsere erwarteten und tatsächlichen Objekte die gleichen Eigenschaften haben. In diesem Fall testet sie, dass die von uns erwarteten Arrays beide ein JavaScript-Objekt beinhalten. Dann überprüft sie, dass ihre JavaScript-Objekte die gleichen Eigenschaften haben, d. h., ihre beiden title-Eigenschaften "run code" und ihre beiden completed-Eigenschaften false sind.

      Dann schließen wir die restlichen Tests unter Verwendung dieser beiden Gleichheitsprüfungen nach Bedarf durch Hinzufügen der folgenden hervorgehobenen Zeilen ab:

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

      Speichern und schließen Sie die Datei.

      Unser Test imitiert nun unseren manuellen Test. Mit diesen programmatischen Tests müssen wir die Ausgabe nicht kontinuierlich überprüfen, um zu sehen, ob unsere Tests bei der Ausführung bestehen. Üblicherweise möchte man jeden Aspekt der Verwendung testen, um sicherzustellen, dass der Code ordnungsgemäß getestet wird.

      Wir führen unseren Test erneut mit npm test aus, um diese bekannte Ausgabe zu erhalten:

      Output

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

      Sie haben nun einen integrierten Test mit dem Mocha-Framework und der assert-Bibliothek eingerichtet.

      Gehen wir nun von einer Situation aus, in der wir unser Modul mit einigen anderen Entwicklern geteilt haben und diese uns jetzt Feedback geben. Viele unserer Benutzer würden sich wünschen, dass die Funktion complete() einen Fehler meldet, wenn bisher noch keine TODOs hinzugefügt wurden. Wir fügen diese Funktionalität in unserer Funktion complete() ein.

      Öffnen Sie index.js in Ihrem Texteditor:

      Fügen Sie der Funktion Folgendes hinzu:

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

      Speichern und schließen Sie die Datei.

      Nun fügen wir einen neuen Test für diese neue Eigenschaft hinzu. Wir wollen verifizieren, ob ein Todos-Objekt, das keine Elemente enthält, unseren speziellen Fehler ausgibt, wenn wir es mit complete aufrufen.

      Gehen Sie in die index.test.js zurück:

      Fügen Sie am Ende der Datei den folgenden Code hinzu:

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

      Wie zuvor verwenden wir describe() und it(). Wir beginnen unseren Test mit der Erstellung eines neuen todos-Objekts. Dann definieren wir den Fehler, dessen Meldung wir erwarten, wenn wir die Funktion complete() aufrufen.

      Als Nächstes verwenden wir die Funktion throws() des assert-Moduls. Diese Funktion wurde erstellt, damit wir die Fehler, die unser Code ausgibt, verifizieren können. Sein erstes Argument ist eine Funktion, die den Code enthält, der den Fehler ausgibt. Das zweite Argument ist der Fehler, dessen Meldung wir erwarten.

      Führen Sie in Ihrem Terminal erneut die Tests mit npm test aus und Sie sehen die folgende Ausgabe:

      Output

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

      Diese Ausgabe zeigt den Nutzen, warum wir automatisiertes Testen mit Mocha und assert durchführen. Da unsere Tests schriftlich ausgearbeitet sind, verifizieren wir bei jeder Ausführung von npm test, dass alle unsere Tests bestehen. Wir mussten nicht manuell überprüfen, ob der andere Code noch funktioniert – wir wissen, dass es so ist, da der Test, den wir haben, bestand.

      Bisher haben unsere Tests die Ergebnisse von synchronem Code verifiziert. Wir behandeln nun, wie wir unsere neu gewonnenen Testgewohnheiten anpassen müssten, um mit asynchronem Code arbeiten zu können.

      Schritt 4 – Testen von asynchronem Code

      Eine der Eigenschaften, die wir in unserem TODO-Modul benötigen, ist eine CSV-Exportfunktion. Damit werden alle gespeicherten TODOs zusammen mit dem abgeschlossenen Status in einer Datei ausgegeben. Das erfordert die Verwendung des Moduls fs – eines integrierten Node.js-Moduls für die Arbeit mit dem Dateisystem.

      Das Schreiben in eine Datei ist eine asynchrone Operation. Es gibt viele Möglichkeiten, in eine Datei in Node.js zu schreiben. Wir können Callbacks, Promises oder die Schlüsselworte async/await verwenden. In diesem Abschnitt behandeln wir, wie wir Tests für diese verschiedenen Methoden schreiben.

      Callbacks

      Eine callback-Funktion ist eine Funktion, die als Argument in einer asynchronen Funktion verwendet wird. Sie wird aufgerufen, wenn die asynchrone Operation abgeschlossen ist.

      Wir fügen unserer Todos-Klasse eine Funktion namens saveToFile() hinzu. Diese Funktion erstellt eine Zeichenfolge, indem Sie alle unsere TODO-Elemente durchläuft, und schreibt diese Zeichenfolge in eine Datei.

      Öffnen Sie Ihre index.js-Datei:

      Fügen Sie den folgenden hervorgehobenen Code in die Datei ein:

      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;
      

      Zunächst müssen wir das fs-Modul in unsere Datei importieren. Dann haben wir unsere neue Funktion saveToFile() hinzugefügt. Unsere Funktion übernimmt eine Callback-Funktion, die genutzt wird, sobald die Schreiboperation der Datei abgeschlossen ist. In dieser Funktion erstellen wir eine fileContents-Variable, die die gesamte zu speichernde Zeichenfolge als Datei speichert. Sie wird mit den CSV-Titeln initialisiert. Dann durchlaufen wir jedes TODO-Element mit der forEach()-Methode des internen Arrays. Beim Durchlaufen fügen wir die title– und completed-Eigenschaften der einzelnen todos hinzu.

      Zum Schluss verwenden wir das fs-Modul zum Schreiben der Datei mit der writeFile()-Funktion. Unser erstes Argument ist der Dateiname: todos.csv. Das zweite ist der Inhalt der Datei, in diesem Fall unsere fileContents-Variable. Das letzte Argument ist unsere Callback-Funktion, die alle Schreibfehler der Datei behandelt.

      Speichern und schließen Sie die Datei.

      Wir schreiben nun einen Test für unsere Funktion saveToFile. Unser Test führt zwei Dinge aus: Er überprüft die Existenz der Datei und verifiziert, dass sie den richtigen Inhalt hat.

      Öffnen Sie die Datei index.test.js:

      Beginnen wir damit, das fs-Modul am Anfang der Datei zu laden, da wir es zum Testen unserer Ergebnisse verwenden werden:

      todos/index.test.js

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

      Am Ende der Datei fügen wir unseren neuen Testfall hinzu:

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

      Wie zuvor verwenden wir describe(), um unseren Test getrennt von den anderen zu gruppieren, da er eine neue Funktionalität enthält. Die it()-Funktion unterscheidet sich leicht von unseren anderen. Normalerweise hat die von uns verwendete Callback-Funktion keine Argumente. Dieses Mal haben wir done als Argument. Wir benötigen dieses Argument, wenn wir Funktionen mit Callbacks testen. Die Callback-Funktion done() wird von Mocha verwendet, um ihr anzugeben, wenn eine asynchrone Funktion abgeschlossen ist.

      Alle in Mocha getesteten Callback-Funktionen müssen den Callback done() aufrufen. Wäre dies nicht der Fall, würde Mocha nie wissen, ob die Funktion abgeschlossen ist und würde festgefahren auf ein Signal warten.

      Wir erstellen nun unsere Todos-Instanz und fügen ihr ein einzelnes Element hinzu. Wir rufen die Funktion saveToFile() mit einem Callback auf, der einen Dateischreibfehler findet. Beachten Sie, wie unser Test für diese Funktion im Callback enthalten ist. Wenn unser Testcode außerhalb des Callbacks wäre, würde er fehlschlagen, solange der Code aufgerufen würde, bevor das Schreiben der Datei abgeschlossen wäre.

      In unserer Callback-Funktion überprüfen wir zunächst, dass unsere Datei existiert:

      todos/index.test.js

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

      Die Funktion fs.existsSync() gibt true aus, wenn der Dateipfad in ihrem Argument existiert, und andernfalls false.

      Anmerkung: Die Funktionen des fs-Moduls sind standardmäßig asynchron. Sie bildeten jedoch für Schlüsselfunktionen synchrone Gegenstücke. Dieser Test ist einfacher, wenn synchrone Funktionen verwendet werden, da wir den asynchronen Code nicht schachteln müssen, um sicherzustellen, dass er funktioniert. Im fs-Modul enden synchrone Funktionen normalerweise mit "Sync" am Ende ihrer Namen.

      Dann erstellen wir eine Variable, um unseren erwarteten Wert zu speichern:

      todos/index.test.js

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

      Wir verwenden readFileSync() des fs-Moduls zum synchronen Lesen der Datei:

      todos/index.test.js

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

      Wir geben readFileSync() den richtigen Pfad für die Datei: todos.csv. Da readFileSync() ein Buffer-Objekt ausgibt, das Binärdaten speichert, verwenden wir seine toString()-Methode, damit wir seinen Wert mit der Zeichenfolge vergleichen können, die wir voraussichtlich gespeichert haben.

      Wie zuvor verwenden wir das strictEqual des assert-Moduls, um einen Vergleich durchzuführen:

      todos/index.test.js

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

      Wir beenden unseren Test durch das Aufrufen des done()-Callbacks, sodass Mocha weiß, dass der Test dieses Falls gestoppt wird:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Wir geben das err-Objekt zu done(), sodass der Test mit Mocha fehlschlägt, falls ein Fehler vorhanden ist.

      Speichern und beenden Sie index.test.js.

      Wie zuvor führen wir diesen Test mit npm test durch. Ihre Konsole zeigt dann diese Ausgabe:

      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)

      Sie haben nun Ihre erste asynchrone Funktion mit Mocha unter der Verwendung von Callbacks getestet. Zum Zeitpunkt des Schreibens dieses Tutorials sind Promises jedoch verbreiteter als Callbacks in neuem Node.js-Code, wie auch in unserem Artikel Schreiben von asynchronem Code in Node.js beschrieben. Als Nächstes lernen wir, wie wir auch diese mit Mocha testen können.

      Promises

      Ein Promise ist ein JavaScript-Objekt, das letztendlich einen Wert ausgibt. Wenn ein Promise erfolgreich ist, ist es gelöst. Wenn es auf einen Fehler trifft, wird es verworfen.

      Wir ändern die saveToFile()-Funktion, damit sie Promises anstelle von Callbacks verwendet. Öffnen Sie index.js:

      Zuerst müssen wir ändern, wie das fs-Modul geladen ist. Ändern Sie in Ihrer index.js-Datei die require()-Aussage am Anfang der Datei, sodass sie aussieht wie folgt:

      todos/index.js

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

      Wir haben nun das fs-Modul importiert, das Promises anstelle von Callbacks verwendet. Nun müssen wir einige Änderungen an saveToFile() vornehmen, damit es stattdessen mit Promises arbeitet.

      Führen Sie in Ihrem Texteditor die folgenden Änderungen an der Funktion saveToFile() aus, um die Callbacks zu entfernen:

      todos/index.js

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

      Der erste Unterschied besteht darin, dass unsere Funktion keine Argumente mehr akzeptiert. Mit Promises benötigen wir keine Callback-Funktion. Die zweite Änderung betrifft die Weise, wie die Datei geschrieben ist. Wir geben nun das Ergebnis des writeFile()-Promises aus.

      Speichern und schließen Sie index.js.

      Wir passen unseren Test so an, dass er mit Promises funktioniert. Öffnen Sie index.test.js:

      Ändern Sie den saveToFile()-Test auf Folgendes:

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

      Als erste Änderung müssen wir den done()-Callback aus den Argumenten entfernen. Wenn Mocha das done()-Argument durchläuft, muss es aufgerufen werden oder es gibt einen Fehler wie folgt aus:

      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)
      

      Schließen Sie beim Testen von Promises den done()-Callback nicht in it() ein.

      Um unser Promise zu testen, müssen wir unseren Assertionscode in die then()-Funktion einfügen. Beachten Sie, dass wir dieses Promise im Test ausgeben. Wir haben keine catch()-Funktion, um das Promise auszufangen, wenn es verworfen wird.

      Wir geben das Promise aus, sodass alle Fehler, die in der then()-Funktion ausgegeben werden, in der it()-Funktion heraustreten. Wenn die Fehler nicht heraustreten, wird der Testfall mit Mocha nicht fehlschlagen. Beim Testen von Promises müssen Sie return auf das getestete Promise verwenden. Andernfalls besteht das Risiko, ein falsch-positives Ergebnis zu erhalten.

      Wir lassen auch die catch()-Klausel aus, da Mocha erkennen kann, wenn ein Promise verworfen wird. Im Fall einer Verwerfung schlägt der Test automatisch fehl.

      Da unser Test nun fertig ist, speichern und beenden Sie die Datei und führen Sie anschließend Mocha mit npm test aus. Als Bestätigung erhalten wir ein erfolgreiches Ergebnis:

      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)

      Wir haben unseren Code und Test zur Verwendung von Promises geändert und wissen nun sicher, dass es funktioniert. Die neuesten asynchronen Muster verwenden jedoch async/await-Schlüsselwörter, damit wir nicht mehrere then()-Funktionen erstellen müssen, um erfolgreiche Ergebnisse zu bearbeiten. Sehen wir als Nächstes, wie wir mit async/await testen können.

      async/await

      Die Schlüsselwörter async/await erleichtern die Arbeit mit Promises, da sie nicht so ausführlich sind. Sobald wir eine Funktion als asynchron mit dem Schlüsselwort async definieren, können wir alle zukünftigen Ergebnisse in dieser Funktion mit dem Schlüsselwort await erhalten. Auf diese Weise können wir Promises verwenden, ohne die Funktionen then() oder catch() verwenden zu müssen.

      Wir können unseren auf Promises basierenden saveToFile()-Test mit async/await vereinfachen. Führen Sie in Ihrem Texteditor diese kleineren Änderungen im saveToFile()-Test in der index.test.js aus:

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

      Die erste Änderung besteht darin, dass die von der it()-Funktion verwendete Funktion jetzt das Schlüsselwort async zur Definierung verwendet. Dadurch können wir das Schlüsselwort await in ihrem Körper verwenden.

      Die zweite Änderung tritt auf, wenn wir saveToFile() aufrufen. Bevor es aufgerufen wird, wird das Schlüsselwort await verwendet. Node.js wird nun warten, bis diese Funktion gelöst ist, bevor es den Test fortsetzt.

      Da wir den Code aus der then()-Funktion in den it()-Funktionskörper verschoben haben, ist unser Funktionscode leichter zu lesen. Die Ausführung dieses Codes mit npm test erzeugt diese Ausgabe:

      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)

      Wir können jetzt asynchrone Funktionen testen, indem wir ein beliebiges von drei asynchronen Paradigmen entsprechend verwenden.

      Wir haben mit dem Testen von synchronem und asynchronem Code mit Mocha schon einen breiten Bereich abgedeckt. Als Nächstes tauchen wir tiefer ein in einige andere Funktionalitäten, die Mocha bietet, um unsere Testerfahrung zu verbessern. Besonders interessant ist hierbei auch, wie Hooks die Testumgebungen verändern können.

      Schritt 5 – Verwenden von Hooks zur Verbesserung von Testfällen

      Hooks sind ein nützlicher Bestandteil von Mocha, der es uns ermöglicht, die Umgebung vor und nach einem Test zu konfigurieren. Wir fügen Hooks typischerweise in einen describe()-Funktionsblock, da diese eine für einige Testfälle spezifische Auf- und Abbaulogik enthalten.

      Mocha bietet vier Hooks, die wir in unseren Tests verwenden können:

      • before: Dieser Hook wird einmal ausgeführt, bevor der erste Test beginnt.
      • beforeEach: Dieser Hook wird vor jedem Testfall ausgeführt.
      • after: Dieser Hook wird einmal ausgeführt, nachdem der letzte Testfall abgeschlossen ist.
      • afterEach: Dieser Hook wird nach jedem Testfall ausgeführt.

      Hooks sind sehr nützlich, wenn wir eine Funktion oder Eigenschaft mehrmals testen, da sie uns erlauben, den Einrichtungscode des Tests (wie das Erstellen des todos-Objekts) vom Assertionscode des Tests zu trennen.

      Um den Wert von Hooks zu sehen, fügen wir dem Testblock saveToFile() weitere Tests hinzu.

      Obwohl wir bestätigt haben, dass wir unsere TODO-Elemente in eine Datei speichern können, haben wir nur ein Element gespeichert. Außerdem wurde das Element nicht als abgeschlossen markiert. Wir fügen weitere Tests hinzu, um sicherzustellen, dass die verschiedenen Aspekte unseres Moduls funktionieren.

      Wir fügen zunächst einen zweiten Test hinzu, um zu bestätigen, dass unsere Datei korrekt gespeichert wird, wenn wir ein abgeschlossenes TODO-Element haben. Öffnen Sie Ihre Datei index.test.js mit Ihrem Texteditor:

      Ändern Sie den letzten Test folgendermaßen:

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

      Der Test ist ähnlich wie zuvor. Die wichtigsten Unterschiede sind, dass wir die complete()-Funktion vor saveToFile() aufrufen, und unsere expectedFileContents nun true anstatt false für den Wert der completed-Kolumne haben.

      Speichern und schließen Sie die Datei.

      Wir führen unseren neuen Test und alle anderen mit npm test aus:

      Dadurch ergibt sich Folgendes:

      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)

      Es funktioniert wie erwartet. Es gibt jedoch Raum für Verbesserungen. Sie müssen ein Todos-Objekt zu Beginn des Tests instanziieren. Beim Hinzufügen von mehr Testfällen wird dies schnell repetitiv und vergeudet Speicherplatz. Außerdem erstellt der Test bei jeder Ausführung eine Datei. Das kann von jemandem, der sich mit dem Modul nicht so gut auskennt, mit einer tatsächlichen Ausgabe verwechselt werden. Es wäre schön, wenn wir unsere Ausgabedateien nach dem Testen bereinigen würden.

      Führen wir nun diese Verbesserungen mit Test-Hooks aus. Wir verwenden den Hook beforeEach(), um unsere Testvorrichtung von TODO-Elementen einzurichten. Eine Testvorrichtung ist jeder konsistente Zustand, der in einem Test verwendet wird. In unserem Fall ist unsere Testvorrichtung ein neues todos-Objekt, dem bereits ein TODO-Element hinzugefügt wurde. Wir verwenden afterEach(), um die vom Test erstellte Datei zu entfernen.

      Führen Sie in index.test.js die folgenden Änderungen an Ihrem letzten Test für saveToFile() aus:

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

      Entschlüsseln wir alle vorgenommenen Änderungen. Wir haben dem Testblock ein beforeEach() hinzugefügt:

      todos/index.test.js

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

      Diese beiden Zeilen Code erstellen ein neues Todos-Objekt, das in jedem unserer Tests verfügbar ist. Mit Mocha verweist das this-Objekt in beforeEach() auf dasselbe this-Objekt in it(). this ist für jeden Codeblock im describe()-Block gleich. Weitere Informationen über this finden Sie in unserem Tutorial Verstehen Sie This, Bind, Call und Apply in JavaScript.

      Diese leistungsstarke gemeinsame Nutzung des Kontexts ist der Grund, warum wir schnell Testvorrichtungen erstellen können, die für unsere beiden Tests funktionieren.

      Dann bereinigen wir unsere CSV-Datei in der afterEach()-Funktion:

      todos/index.test.js

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

      Wenn unser Test fehlgeschlagen ist, hat der Test möglicherweise keine Datei erstellt. Aus diesem Grund überprüfen wir, ob die Datei vorhanden ist, bevor wir die Funktion unlinkSync() verwenden, um diese zu löschen.

      Die verbleibenden Änderungen wechseln die Referenz von todos, die zuvor in der Funktion it() erstellt wurden, zu this.todos, das im Mocha-Kontext verfügbar ist. Wir haben auch die Zeilen gelöscht, die zuvor todos in den einzelnen Testfällen instanziierten.

      Führen wir nun diese Datei aus, um zu bestätigen, dass unsere Tests noch funktionieren. Geben Sie npm test in Ihr Terminal ein, um Folgendes zu erhalten:

      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)

      Die Ergebnisse sind gleich, und als zusätzlichen Vorteil haben wir die Einrichtungszeit für neue Tests für die Funktion saveToFile() leicht reduziert sowie eine Lösung für die zurückbleibende CSV-Datei gefunden.

      Zusammenfassung

      In diesem Tutorial haben Sie ein Node.js-Modul geschrieben, um TODO-Elemente zu verwalten und den Code manuell mit der Node.js-REPL getestet. Dann haben Sie eine Testdatei erstellt und das Mocha-Framework zur Ausführung automatisierter Tests verwendet. Mit dem assert-Modul konnten Sie verifizieren, ob Ihr Code funktioniert. Sie haben mit Mocha auch synchrone und asynchrone Funktionen getestet. Schließlich haben Sie Hooks mit Mocha erstellt, die das Schreiben mehrerer verwandter Testfälle wesentlich lesbarer und wartungsfreundlicher machen.

      Mit diesen neuen Kenntnissen können Sie nun versuchen, Tests für neue Node.js-Module zu schreiben, die Sie gerade erstellen. Können Sie über die Ein- und Ausgaben Ihrer Funktion nachdenken und Ihren Test schreiben, bevor Sie Ihren Code erstellen?

      Wenn Sie weitere Informationen über das Mocha-Framework erhalten möchten, besuchen Sie unsere offizielle Mocha-Dokumentation. Wenn Sie gerne noch mehr über Node.js lernen möchten, können Sie zu der Serienseite Codieren in Node.js zurückkehren.



      Source link

      Cómo probar un módulo de Node.js con Mocha y Assert


      El autor seleccionó a Open Internet/Free Speech Fund para recibir una donación como parte del programa Write for DOnations.

      Introducción

      Las pruebas son una parte integral del desarrollo de software. Es común para los programadores ejecutar código para probar su aplicación a medida que realizan cambios y así confirmar que se se comporta como se espera. Con la configuración de prueba correcta, este proceso puede incluso automatizarse y ahorrar mucho tiempo. Ejecutar pruebas de manera uniforme tras escribir nuevo código garantiza que los nuevos cambios no afecten las funciones preexistentes. Esto proporciona al desarrollador confianza en su base de código, en especial cuando se implementa en la producción para que los usuarios puedan interactuar con ella.

      Un marco de prueba estructura la forma en que creamos casos de prueba. Mocha es un marco de prueba de JavaScript muy popular que organiza nuestros casos de prueba y se encarga de ejecutarlos. Sin embargo, no verifica el comportamiento de nuestro código. Para comparar los valores en una prueba, podemos usar el módulo assert de Node.js.

      En este artículo, escribirá pruebas para un módulo de lista TODO de Node.js. Configurará y usará el marco de prueba Mocha para estructurar sus pruebas. A continuación, usará el módulo assert Node.js para crear las pruebas propiamente dichas. En este sentido, usará Mocha como creador del plan, y assert para implementar el plan.

      Requisitos previos

      Paso 1: Escribir un módulo Node

      Nuestro primer paso para este artículo será escribir el módulo Node.js que nos gustaría probar. Este módulo administrará una lista de elementos TODO. Con este módulo podremos enumerar todos los elementos TODO de los que realizamos un seguimiento, añadir nuevos elementos y marcar algunos como completados. Además, podrá exportar una lista de elementos TODO a un archivo CSV. Si desea repasar la manera de escribir módulos de Node.js, puede leer nuestro artículo Cómo crear un módulo de Node.js.

      Primero, debemos configurar el entorno de codificación. Cree una carpeta con el nombre de su proyecto en su terminal. En este tutorial se usará el nombre todos:

      Luego acceda a esa carpeta:

      Ahora, inicie npm. Usaremos su funcionalidad CLI para ejecutar las pruebas más tarde:

      Solo tenemos una dependencia, Mocha, que usaremos para organizar y ejecutar nuestras pruebas. Para descargar e instalar Mocha, utilice lo siguiente:

      • npm i request --save-dev mocha

      Instalamos Mocha como dependencia dev, ya que en un entorno de producción el módulo no lo necesita. Si desea obtener más información sobre los paquetes de Node.js o npm, consulte nuestra guía Cómo usar módulos Node.js con npm y package.json.

      Finalmente, crearemos nuestro archivo que contendrá el código de nuestro módulo:

      Con esto, estaremos listos para crear nuestro módulo. Abra index.js en un editor de texto; por ejemplo, nano:

      Comenzaremos definiendo la clase Todos. Esta clase contiene todas las funciones que necesitamos para administrar nuestra lista TODO. Añada las siguientes líneas de código a index.js:

      todos/index.js

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

      Comenzamos el archivo creando una clase Todos. Su función constructor() no toma argumentos. Por lo tanto, no es necesario proporcionar valores para crear instancias de un objeto para esta clase. Lo único que hacemos cuando iniciamos un objeto Todos es crear una propiedad todos, que es una matriz vacía.

      La línea modules permite que otros módulos de Node.js requieran nuestra clase Todos. Si no se exporta explícitamente la clase, el archivo de prueba que crearemos más tarde no podrá usarla.

      Añadiremos una función para mostrar la matriz de todos que almacenamos. Escriba las siguientes líneas resaltadas:

      todos/index.js

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

      Nuestra función list() devuelve una copia de la matriz usada por la clase. Hace una copia de la matriz usando la sintaxis de desestructuración de JavaScript. Creamos una copia de la matriz de modo que los cambios que el usuario realice en la matriz y se muestren a través de list() no afecten a la matriz usada por el objeto Todos.

      Nota: Las matrices de JavaScript son tipos de referencia. Esto significa que para cualquier asignación de variable a una matriz o invocación de función con una matriz como parámetro, JavaScript consulta la matriz original que se creó. Por ejemplo, si tenemos una matriz con tres elementos llamada x, y creamos una nueva variable y de modo que y = x, y y x hacen referencia a lo mismo. Cualquier cambio que realicemos a la matriz con y afecta a la variable x y viceversa.

      Ahora escribiremos la función add(), que añade un nuevo elemento 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;
      

      Nuestra función add() toma una cadena y la dispone en una nueva propiedad title del objeto de JavaScript. El nuevo objeto también tiene una propiedad completed, que se fija en false por defecto. Luego añadimos este nuevo objeto a nuestra matriz de TODO.

      Una funcionalidad importante en un administrador de TODO es la de marcar los elementos como completados. Para esta implementación, repetiremos nuestra matriz todos para hallar el elemento TODO que el usuario está buscando. Si se encuentra uno, lo marcaremos como completado. Si no se encuentra ninguno, mostraremos un error.

      Añada la función complete() de esta 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;
      

      Guarde el archivo y cierre el editor de texto.

      Ahora disponemos de un administrador TODO básico con el que podemos experimentar. A continuación, probaremos nuestro código manualmente para ver si la aplicación funciona.

      Paso 2: Probar el código de forma manual

      En este paso, ejecutaremos las funciones de nuestro código y observaremos el resultado para garantizar que cumpla con nuestras expectativas. Esto se conoce como prueba manual. Es probablemente la metodología de prueba más común que aplican los programadores. Aunque automatizaremos nuestra prueba más tarde con Mocha, primero probaremos manualmente nuestro código para dar un mejor sentido a cómo la prueba manual difiere de los marcos de prueba.

      Añadiremos dos elementos TODO a nuestra aplicación y marcaremos uno como completado. Inicie el REPL de Node.js en la misma carpeta que el archivo index.js:

      Verá el símbolo > en el REPL que dice que podemos ingresar código de JavaScript. Escriba lo siguiente en el símbolo:

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

      Con require(), cargamos el módulo TODOs en una variable Todos. Recuerde que nuestro módulo muestra la clase Todos por defecto.

      Ahora, vamos a instanciar un objeto para esa clase. En el REPL, añada esta línea de código:

      • const todos = new Todos();

      Podemos usar el objeto todos para verificar que nuestra implementación funcione. Añadiremos nuestro primer elemento TODO:

      Hasta ahora, no vimos ningún resultado en nuestro terminal. Verificaremos que almacenamos nuestro elemento TODO "run code" obteniendo una lista de todos nuestros TODO:

      Verá este resultado en su REPL:

      Output

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

      Este es el resultado esperado: tenemos un elemento TODO en nuestra matriz de TODO y no está completado por defecto.

      Vamos añadiremos otro elemento TODO:

      • todos.add("test everything");

      Marque el primer elemento TODO como completado:

      • todos.complete("run code");

      Nuestro objeto todos ahora estará administrando dos elementos: "run code" y "test everything". El TODO "run code" también se completará. Confirmaremos esto invocando list() de nuevo:

      El REPL mostrará el siguiente resultado:

      Output

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

      Ahora, salga del REPL con lo siguiente:

      Hemos confirmado que nuestro módulo se comporta como esperamos. Aunque no dispusimos nuestro código en un archivo de prueba o usamos una biblioteca de prueba, probamos nuestro código manualmente. Desafortunadamente, esta forma de prueba requiere mucho tiempo cada vez que se realiza un cambio. A continuación, utilizaremos las pruebas automatizadas en Node.js y veremos si podemos resolver este problema con el marco de prueba Mocha.

      Paso 3: Escribir su primera prueba con Mocha y Assert

      En el último paso, probamos nuestra aplicación de forma manual. Esto funcionará para los casos de uso individuales, pero a medida que nuestro módulo se amplía, este método se vuelve menos viable. A medida que probemos nuevas funciones, debemos estar seguros de que la funcionalidad añadida no genere problemas con la funcionalidad anterior. Nos gustaría probar cada función una y otra vez para cada cambio del código, pero hacer esto a mano tomaría mucho esfuerzo y podría producir a errores.

      Una práctica más eficiente consistiría en configurar pruebas automatizadas. Estas son pruebas con secuencias de comandos escritas como cualquier otro bloque de código. Ejecutamos nuestras funciones con entradas definidas e inspeccionamos sus efectos para garantizar que se comporten como esperamos. A medida que nuestra base de código se amplíe, lo mismo sucederá con nuestras pruebas automatizadas. Cuando escribimos nuevas pruebas junto a las funciones, podemos verificar que el módulo completo aún funcione, todo sin necesidad de recordar la manera en que se usa cada función en cada ocasión.

      En este tutorial, usaremos el marco de pruebas Mocha con el módulo assert de Node.js. Recurriremos a la experiencia práctica para ver cómo funcionan juntos.

      Para comenzar, cree un nuevo archivo para almacenar nuestro código de prueba:

      Ahora, utilice su editor de texto para abrir el archivo de prueba. Puede usar nano como antes:

      En la primera línea del archivo de texto, cargaremos el módulo TODO como hicimos en el shell de Node.js. Luego cargaremos el módulo assert para cuando escribamos nuestras pruebas. Añada las siguientes líneas:

      todos/index.test.js

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

      La propiedad strict del módulo assert nos permitirá usar las pruebas de igualdad especiales que se recomiendan desde Node.js y son adecuadas para una buena protección futura, ya que tienen en cuenta la mayoría de los casos de uso.

      Antes de pasar a las pruebas de escritura, veremos cómo Mocha organiza nuestro código. Las pruebas estructuradas en Mocha normalmente siguen esta plantilla:

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

      Observe las dos funciones principales: describe() e it(). La función describe() se usa para agrupar pruebas similares. No es necesario para que Mocha ejecute las pruebas, pero agrupar las pruebas facilita el mantenimiento de nuestro código. Se recomienda que agrupe sus pruebas de una manera que le permita actualizar fácilmente las que son similares.

      it() contiene nuestro código de prueba. Aquí interactuaremos con las funciones de nuestro módulo y usaremos la bibliteca assert. Muchas funciones it() pueden definirse en una función describe().

      Nuestro objetivo en esta sección es usar Mocha y assert para automatizar nuestra prueba manual. Haremos esto paso a paso, comenzando con nuestro bloque de descripción. Añada lo siguiente a su archivo después de las líneas del módulo:

      todos/index.test.js

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

      Con este bloque de código, hemos creado una agrupación para nuestras pruebas integradas. Las pruebas Unit probarán una función a la vez. Las pruebas de integración verifican si las funciones en o entre los módulos funcionan bien juntas. Cuando Mocha ejecute nuestras pruebas, todas las pruebas en ese bloque de descripción se ejecutarán en el grupo "integration test".

      Añadiremos una función it() para que podamos comenzar a probar el código de nuestro módulo.

      todos/index.test.js

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

      Observe que hicimos que el nombre de la prueba sea descriptivo. Si alguien ejecuta nuestra prueba, quedará claro de inmediato qué supera la prueba y qué no. Una aplicación bien probada normalmente es una aplicación bien documentada, y las pruebas pueden ser a veces un tipo de documentación efectivo.

      Para nuestra primera prueba, crearemos un nuevo objeto Todos y verificaremos que no tenga elementos.

      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 primera nueva línea de código creó una instancia de un nuevo objeto Todos como haríamos en el REPL de Node.js o en otro módulo. En la segunda nueva línea, usamos el módulo assert.

      Desde el módulo assert usamos el método notStrictEqual(). Esta función toma dos parámetros: el valor que deseamos probar (llamado actual) y el valor que esperamos obtener (llamado expected). Si ambos argumentos son iguales, notStrictEqual() genera un error para que la prueba falle.

      Guarde y cierre index.test.js.

      El caso básico será true, ya que la extensión debería ser 0, que no es 1. Vamos a confirmar esto ejecutando Mocha. Para hacer esto, debemos modificar nuestro archivo package.json. Abra el archivo package.json con su editor de texto:

      Ahora, en su propiedad scripts, cámbielo para que tenga este aspecto:

      todos/package.json

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

      Acabamos de cambiar el comportamiento del comando CLI test de npm. Cuando ejecutemos npm test, npm revisará el comando que acabamos de introducir en package.json. Buscará la biblioteca Mocha en nuestra carpeta node_modules y ejecutará el comando mocha con nuestro archivo de prueba.

      Guarde y cierre package.json.

      Veamos qué sucede cuando ejecutemos nuestra prueba. En su terminal, introduzca lo siguiente:

      El comando producirá el siguiente resultado:

      Output

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

      Este resultado primero nos muestra el grupo de pruebas que está a punto de ejecutarse. Para cada prueba individual en un grupo, se marca el caso de prueba. Vemos el nombre de nuestra prueba como lo describimos en la función it(). La marca de verificación en el lado izquierdo del caso de prueba indica que se superó la prueba.

      En la parte inferior, vemos un resumen de todas nuestras pruebas. En nuestro caso, nuestra prueba fue superada y se completó en 16 ms (el tiempo varía según el equipo).

      Nuestra prueba se inició correctamente. Sin embargo, este caso de prueba actual puede dar lugar a instancias de “falsos positivos”. Un falso positivo es un caso en el que se supera la prueba cuando esto no debería suceder.

      Actualmente comprobamos que la extensión de la matriz no sea igual a 1. Modificaremos la prueba de modo que esta condición sea “true” cuando este no debería ser el caso. Añada las siguientes líneas a 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);
          });
      });
      

      Guarde el archivo y ciérrelo.

      Añadimos dos elementos TODO. Ejecutaremos la prueba para ver qué sucede:

      Esto dará el siguiente resultado:

      Output

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

      La prueba se supera, como se espera, ya que la extensión es mayor a 1. Sin embargo, se frustra la finalidad original de contar con esa primera prueba. La primera prueba está destinada a confirmar que empezamos en un estado vacío. Una mejor prueba confirmará eso en todos los casos.

      Cambiaremos la prueba de modo que solo se supere si no tenemos ningú´n TODO. Aplique los siguientes cambios en 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);
          });
      });
      

      Cambió notStrictEqual() por strictEqual(), una función que comprueba la igualdad entre su argumento real y esperado. “strictEqual” fallará si nuestros argumentos no son exactamente los mismos.

      Guarde y cierre el archivo, y ejecute la prueba para que podamos ver lo que sucede:

      Esta vez, en el resultado se mostará un error:

      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 nos resultará útil para determinar por qué falló la prueba mediante depuración. Observe que, dado que la prueba falló, no apareció una marca de verificación al principio del caso de prueba.

      Nuestro resumen de prueba ya no está en la parte inferior del resultado, sino justo después de donde nuestra lista de casos de prueba se mostraba:

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

      El resultado restante nos proporciona datos sobre las pruebas no superadas. Primero, vemos el caso de prueba que falló:

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

      A continuación, vemos por qué falló nuestra prueba:

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

      Se presenta un AssertionError cuando strictEqual() falla. Vemos que el valor expected, 0, es diferente el valor actual.

      A continuación vemos la línea en nuestro archivo de prueba en el que falla el código. En este caso, es la línea 10.

      Ahora, vimos que nuestra prueba fallará si esperamos valores incorrectos. Cambiaremos nuestro caso de prueba de nuevo a su valor correcto. Primero, abra el archivo:

      A continuación, elimine las líneas todos.add de modo que su código tenga el siguiente aspecto:

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

      Guarde el archivo y ciérrelo.

      Ejecútelo una vez más para confirmar que supere la prueba sin posibles instancias de “falsos positivos”:

      El resultado será el siguiente:

      Output

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

      Con esto, mejoramos bastante la resistencia de nuestra prueba. Continuaremos con nuestra prueba de integración. El siguiente paso es añadir un nuevo elemento TODO a 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}]);
          });
      });
      

      Tras utilizar la función add(), confirmamos que ahora tenemos un TODO administrado por nuestro objeto todos con strictEqual(). Nuestra siguiente prueba confirma los datos en todos con deepStrictEqual(). La función deepStrictEqual() prueba de forma recursiva que nuestros objetos esperados y reales tienen las mismas propiedades. En este caso, prueba que las matrices que esperamos tengan un objeto JavaScript en ellas. Luego comprueba que sus objetos JavaScript tengan las mismas propiedades; es decir, que sus propiedades titles sean "run code" y sus propiedades completed sean false.

      A continuación, completaremos las pruebas restantes con estas dos comprobaciones de igualdad, según sea necesario, añadiendo las siguientes líneas resaltadas:

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

      Guarde el archivo y ciérrelo.

      Nuestra prueba ahora imita a nuestra prueba manual. Con estas pruebas mediante programación, no es necesario comprobar el resultado continuamente si nuestras pruebas son superadas cuando las ejecutamos. Normalmente, le convendrá probar todos los aspectos de uso para garantizar que el código se pruebe adecuadamente.

      Ejecutaremos nuestra prueba con npm test una vez más para obtener este resultado familiar:

      Output

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

      Con esto, habrá configurado una prueba integrada con el marco Mocha y la biblioteca assert.

      Consideraremos una situación en la que haya compartido nuestro módulo con otros desarrolladores y ahora nos den su opinión. Una buena parte de nuestros usuarios desearán que la función complete() muestre un error si no se añadieron TODO aún. Añadiremos esta funcionalidad en nuestra función complete().

      Abra index.js en su editor de texto:

      Añada lo siguiente a la función:

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

      Guarde el archivo y ciérrelo.

      Ahora, añadiremos una nueva prueba para esta nueva función. Queremos verificar que si invocamos “completed” en un objeto Todos que no tiene elementos, mostrará nuestro error especial.

      Vuelva a index.test.js:

      Al final del archivo, añada el siguiente 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 describe() y it() como antes. Nuestra prueba comienza con la creación de un nuevo objeto todos. A continuación, definimos el error que estamos esperando ver cuando invocamos la función complete().

      A continuación, usamos la función throws() del módulo assert. Esta función se creó para que podamos verificar los errores que se producen en nuestro código. Su primer argumento es una función que contiene el código que produce el error. El segundo argumento es el error que estamos esperando ver.

      En su terminal, ejecute las pruebas con npm test de nuevo. Ahora verá el siguiente resultado:

      Output

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

      En este resultado se resalta el beneficio que nos trae realizar pruebas automatizadas con Mocha y assert. Debido a que nuestras pruebas están programadas, cada vez que ejecutamos npm test verificamos que todas nuestras pruebas se superan. No fue necesario comprobar manualmente si el otro código aún funciona; sabemos que es así porque las pruebas que tenemos se han superado.

      Hasta ahora, nuestras pruebas han verificado los resultados del código sincronizado. Vamos a ver cómo deberíamos adaptar nuestros nuevos hábitos de prueba para que funcionen con el código asíncrono.

      Paso 4: Probar código asíncrono

      Una de las funciones que deseamos en nuestro módulo TODO es una función de exportación de CSV. Imprimirá en un archivo todos los TODO que tenemos guardados junto con el estado “completed”. Esto requiere que usemos el módulo fs: un módulo de Node.js integrado para trabajar con el sistema de archivos.

      La escritura en un archivo es una operación asíncrona. Existen muchas formas de escribir en un archivo en Node.js. Podemos usar callbacks, promesas o las palabras claves async/await. En esta sección, veremos la manera de escribir pruebas para esos métodos diferentes.

      Callbacks

      Una función callback es la que se utiliza como argumento para una función asíncrona. Se invoca cuando se completa la operación asíncrona.

      Añadiremos una función a nuestra clase Todos llamada saveToFile(). Esta función creará una cadena realizando un bucle con todos nuestros elementos TODO y escribiendo esa cadena en un archivo.

      Abra su archivo index.js:

      En este archivo, añada el siguiente código resaltado:

      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;
      

      Primero tenemos que importar el módulo fs en nuestro archivo. Luego añadimos nuestra nueva función saveToFile(). Nuestra función toma una función callback que se utilizará una vez que se complete la operación de escritura del archivo. En esa función, creamos una variable fileContents que almacena toda la cadena que queremos guardar como archivo. Se inicializa con los encabezados de CSV. A continuación, realizamos un bucle con cada elemento TODO con el método forEach() de la matriz interna. A medida que realizamos iteraciones, añadimos las propiedades title y completed de los objetos todos individuales.

      Finalmente, usamos el módulo fs para escribir el archivo con la función writeFile(). Nuestro primer argumento es el nombre del archivo: todos.csv. El segundo es el contenido del archivo. En este caso, nuestra variable fileContents. Nuestro último argumento es nuestra función callback, que gestiona cualquier error de escritura.

      Guarde el archivo y ciérrelo.

      Ahora escribiremos una prueba para nuestra función saveToFile(). Nuestra prueba hará dos acciones: confirmar que el archivo existe en primer lugar y luego verificar que su contenido sea correcto.

      Abra el archivo index.test.js:

      Comenzaremos cargando el módulo fs en la parte superior del archivo, ya que lo usaremos para ayudar a probar nuestros resultados:

      todos/index.test.js

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

      Ahora, al final del archivo, añadiremos nuestro caso de prueba:

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

      Como antes, usamos describe() para agrupar nuestras pruebas por separado respecto de las otras, ya que implica una nueva funcionalidad. La función it() se diferencia ligeramente de las otras. Normalmente, la función callback que usamos no tiene argumentos. Esta vez, tenemos done como argumento. Necesitamos este argumento cuando probemos funciones con callbacks. Mocha utiliza la función de callback done() para indicarle cuando se completa una función asíncrona.

      Todas las funciones de callback probadas en Mocha deben invocar el callback done(). Si no es así, Mocha nunca sabría cuándo se completó la función y se quedaría esperando una señal.

      Para continuar, creamos nuestra instancia de Todos y añadimos un único elemento a ella. Luego, invocamos la función saveToFile() con un callback que captura un error de escritura de archivo. Observe que nuestra prueba para esta función reside en el callback. Si nuestro código de prueba estuviera fuera del callback, fallaría siempre que se invocase antes de completar la escritura del archivo.

      En nuestra función de callback, primero comprobamos que nuestro archivo existe:

      todos/index.test.js

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

      La función fs.existsSync() muestra true si la ruta del archivo en su argumento existe; de lo contrario, será false.

      Nota: Las funciones del módulo fs son asíncronas por defecto. Sin embargo, para las funciones claves, crean equivalentes síncronos. Esta prueba es más sencilla al usar las funciones síncronas, ya que no necesitamos anidar el código asíncrono para garantizar que funcione. En el módulo fs, las funciones síncronas normalmente terminan con "Sync" al final de sus nombres.

      A continuación, crearemos una variable para guardar nuestro valor esperado:

      todos/index.test.js

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

      Usamos readFileSync() del módulo fs para leer el archivo de forma síncrona:

      todos/index.test.js

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

      Ahora, proporcionamos readFileSync() con la ruta correcta para el archivo: todos.csv. A medida que readFileSync() muestre el objeto Buffer, que almacena datos binarios, usamos su método toString() para poder comparar su valor con la cadena que esperamos tener guardada.

      Como antes, usamos el strictEqual del módulo assert para realizar una comparación:

      todos/index.test.js

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

      Terminamos nuestra prueba invocando el callback done() y garantizamos que Mocha sepa detener la prueba de ese caso:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Proporcionamos el objeto err a done() de modo que Mocha pueda fallar en la prueba en caso de que se produzca un error.

      Guarde y cierre index.test.js.

      Ejecutaremos esta prueba con npm test como antes. En su consola se mostrará 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)

      De esta manera, habrá probado su primera función asíncrona con Mocha usando callbacks. Sin embargo, en el momento en que se redactó este tutorial las promesas eran más prevalentes que los callbacks en el nuevo código Node.js, como se explica en nuestro artículo Cómo escribir código asíncrono en Node.js. A continuación, aprenderá a pobarlos con Mocha también.

      Promesas

      Una promesa es un objeto de JavaSript que, llegado el momento, mostrará un valor. Cuando una promesa se realiza correctamente, se resuelve. Cuando en ella se produce un error, se rechaza.

      Modificaremos la función saveToFile() para que utilice promesas en vez de callbacks. Abra index.js:

      Primero, debemos cambiar la forma en que se carga el módulo fs. En su archivo index.js, cambie la instrucción require() en la parte superior del archivo de modo que tenga este aspecto:

      todos/index.js

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

      Acabamos de importar el módulo fs, que usa promesas en vez de callbacks. Ahora, debemos realizar algunos cambios en saveToFile() para que funcione con promesas.

      En su editor de texto, aplique los siguientes cambios a la función saveToFile() para eliminar los 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);
      }
      ...
      

      La primera diferencia es que nuestra función ya no acepta ningún argumento. Con promesas no necesitamos una función de callback. El segundo cambio hace referencia a la forma en que se escribe el archivo. Ahora, devolvemos el resultado de la promesa writeFile().

      Guarde y cierre index.js.

      Ahora adaptaremos nuestra prueba de modo que funcione con promesas. Abra index.test.js:

      Cambie la prueba saveToFile() por lo siguiente:

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

      El primer cambio que debemos realizar es eliminar el callback done() de sus argumentos. Si Mocha pasa el argumento done(), debe invocarse. De lo contrario, generará un error como este:

      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)
      

      Al probar promesas, no incluya el callback done() en it().

      Para probar nuestra promesa, debemos disponer nuestro código de aserción en la función then(). Observe que mostramos esta promesa en la prueba y que no tenemos una función catch() que capturar cuando se rechaza la Promise.

      Mostramos la promesa para que cualquier error que aparezca en la función then() se extienda a la función it(). Si los errores no aparecen, Mocha no hará que el caso de prueba fracase. Al probar promesas, deberá usar return en la Promise que se esté probando. Si no es así, tendrá el riesgo de obtener una instancia de falso positivo.

      También omitimos la clausula catch() porque Mocha puede detectar los casos en que se rechaza una promesa. Si se rechaza, automáticamente no superará la prueba.

      Ahora que nuestra prueba está lista, guarde y cierre el archivo y luego ejecute Mocha con npm test, también para confirmar que el resultado sea satisfactorio:

      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)

      Cambianis nuestro código y la prueba para usar promesas y ahora sabemos con seguridad que funciona. Sin embargo, los patrones asíncronos más recientes utilizan las palabras claves async y await a fin de que no tengamos que crear varias funciones then() para gestionar resultados satisfactorios. Vamos a ver cómo podemos hacer la prueba con async y await.

      async/await

      Las palabras claves async y await hacen que trabajar con promesas sea menos complicado. Una vez que definimos una función como asíncrona con la palabra clave async, podemos obtener cualquier resultado futuro en esa función con la palabra clave await. De esta forma, podemos usar promesas sin tener que usar las funciones then() o catch().

      Podemos simplificar nuestra prueba saveToFile() basada en promesas con async y await. En su editor de texto, realice estas pequeñas ediciones en la prueba saveToFile() en 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);
          });
      });
      

      El primer cambio es que la función usada por la función it() ahora tiene la palabra clave async cuando se define. Esto nos permite usar la palabra clave await dentro de su cuerpo.

      El segundo cambio se encuentra cuando invocamos saveToFile(). La palabra clave await se usa antes de se invocación. Ahora, Node.js sabe que debe esperar hasta que se resuelva esta función antes de continuar con la prueba.

      Nuestro código de función es más fácil de leer ahora que movimos el código que estaba en la función then() al cuerpo de la función it(). Ejecutar este código con npm test produce 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)

      Ahora podemos probar las funciones asíncronas usando cualquiera de estos paradigmas asíncronos de forma apropiada.

      Abarcamos un terreno amplio con las pruebas de código síncrono y asíncrono a través de Mocha. A continuación, profundizar en otras funcionalidades que ofrece Mocha para mejorar nuestra experiencia de prueba; en particular, la forma en que los enlaces pueden cambiar entornos de prueba.

      Paso 5: Usar enlaces para mejorar casos de prueba

      Los enlaces son una función útil de Mocha que nos permite configurar el entorno antes y después de una prueba. Normalmente, añadimos hooks en un bloque de funciones describe(), ya que contienen lógica de configuración y desglose específica para algunos casos de prueba.

      Mocha ofrece cuatro enlaces que podemos usar en nuestras pruebas:

      • before: este enlace se ejecuta una vez antes de que se inicie la primera prueba.
      • beforeEach: este se ejecuta antes de cada caso de prueba.
      • after: este se ejecuta una vez después que se completa el último caso de prueba.
      • afterEach: este se ejecuta después de cada caso de prueba.

      Cuando probamos una función o característica varias veces, los enlaces son útiles, ya que nos permiten separar el código de configuración de la prueba (como la creación del objeto todos) desde el código de aserción de esta.

      Para ver el valor de los enlaces, añadiremos más pruebas a nuestro bloque de pruebas saveToFile().

      Aunque confirmamos que podemos guardar nuestros elementos TODO en un archivo, solo guardamos un elemento. Además, el elemento no se marcó como completado. Añadiremos más pruebas para asegurarnos de que los diferentes aspectos de nuestro módulo funcionen.

      Primero, añadiremos una segunda prueba para confirmar que nuestro archivo se guarde correctamente cuando hayamos completado un elemento TODO. Abra el archivo index.test.js en su editor de texto:

      Cambie la última prueba para obtener lo siguiente:

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

      La prueba es similar a lo que teníamos antes. Las principales diferencias radican en que invocamos la función complete() antes de invocar saveToFile() y que ahora el valor de nuestros expectedFileContents es true en vez de false para el valor de la columna completed.

      Guarde el archivo y ciérrelo.

      Ejecutaremos nuestra nueva prueba y todas las demás con npm test:

      Esto dará el siguiente 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 ✓ should save a single TODO that's completed 4 passing (26ms)

      Funciona como se espera. Sin embargo, esto se puede mejorar. Ambas tienen que crear una instancia de un objeto Todos al principio de la prueba. A medida que añadimos más casos de prueba, esto rápidamente se vuelve repetitivo y desperdicia mucha memoria. Además, cada vez que ejecutamos la prueba se crea un archivo. Alguien menos familiarizado con el módulo puede confundir esto con un resultado real. Estaría bien que limpiásemos nuestros archivos resultantes tras la prueba.

      Realizaremos estas mejoras usando enlaces de pruebas. Usaremos el enlace beforeEach() para configurar nuestro artefacto de prueba de elementos TODO. Un artefacto de prueba es cualquier estado uniforme que se usa en una prueba. En nuestro caso, nuestro artefacto de prueba es un nuevo objeto todos que tiene un elemento TODO ya añadido. A continuación usaremos afterEach() para eliminar el archivo creado por la prueba.

      En index.test.js, realice los siguientes cambios a su última prueba para 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);
          });
      });
      

      Desglosaremos todos los cambios que realizamos. Añadimos un bloque beforeEach() al bloque de pruebas:

      todos/index.test.js

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

      Estas dos líneas de código crean un nuevo objeto Todos que estará disponible en cada una de nuestras pruebas. Con Mocha, el objeto this en beforeEach() hace referencia al mismo objeto this en it(). this es el mismo en todos los bloques de código dentro del bloque describe(). Para obtener más datos sobre this, consulte nuestro tutorial Información sobre This, Bind, Call y Apply en JavaScript.

      Este potente intercambio de contexto es el motivo por el que podemos crear rápidamente artefactos que funcionarán para nuestras dos pruebas.

      A continuación, limpiaremos nuestro archivo CSV en la función afterEach():

      todos/index.test.js

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

      Si nuestra prueba falló, es posible que no haya creado un archivo. Por eso verificamos si el archivo existe antes de usar la función unlinkSync() para eliminarlo.

      Los cambios restantes cambian la referencia de todos, creados previamente en la función it(), a this.todos, que está disponible en el contexto de Mocha. También eliminamos las líneas que previamente crearon instancias de todos en los casos de prueba individuales.

      Ahora, ejecutaremos este archivo para confimar que nuestras pruebas aún funcionen. Introduzca npm test en su terminal para obtener lo siguiente:

      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)

      Los resultados son los mismos y, como beneficio, redujimos ligeramente el tiempo de configuración para las nuevas pruebas de la función saveToFile() y encontramos una solución para el archivo CSV residual.

      Conclusión

      A lo largo de este tutorial , escribió un módulo de Node.js para administrar los elementos TODO y probó el código manualmente usando el REPL de Node.js. Luego, creó un archivo de prueba y usó el marco Mocha para ejecutar pruebas automatizadas. Con el módulo assert, pudo verificar que su código funciona. También probó funciones síncronas y asíncronas con Mocha. Finalmente, creó enlaces con Mocha que aportan legibilidad y una capacidad de mantenimiento muy superiores al escribir varios casos de prueba relacionados.

      Ahora que cuenta con este conocimiento, anímese a escribir pruebas para nuevos módulos de Node.js que cree. ¿Puede pensar en las entradas y los resultados de su función y escribir su prueba antes que su código?

      Si desea más información sobre el marco de prueba Mocha, consulte la documentación oficial de Mocha. Si desea seguir aprendiendo sobre Node.js, puede volver a la página de la serie Cómo desarrollar código en Node.js.



      Source link

      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

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

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

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

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

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

      todos/index.test.js

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

      Enregistrez et quittez le fichier.

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

      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