mitmario.dev

node:test

Node.js Sandbox 5 Min Lesezeit 3 BeispieleLektion 1 von 6

Dieser Kurs prüft von der ersten Lektion an mit Testfällen. Jede Aufgabe hat eine Prüfliste unter dem Aufgabentext, jede Zeile darin ist eine Behauptung über dein Programm, und rot heißt, dass sie nicht stimmt. Was du bisher benutzt hast, schreibst du jetzt selbst.

Kein Paket nötig

Node bringt einen Testläufer mit. Der Aufwand, ihn zu benutzen, besteht aus zwei Importen.

Drei Tests, einer davon rot
import test from "node:test";
import assert from "node:assert/strict";

function summe(zahlen) {
  return zahlen.reduce((summe, zahl) => summe + zahl, 0);
}

test("addiert mehrere Zahlen", () => {
  assert.equal(summe([1, 2, 3]), 6);
});

test("gibt bei einer leeren Liste 0 zurueck", () => {
  assert.equal(summe([]), 0);
});

test("dieser hier stimmt absichtlich nicht", () => {
  assert.equal(summe([1, 1]), 3);
});

process.on("exit", (code) => console.log(`Exit-Code: ${code}`));

test() nimmt einen Namen und eine Funktion. Wirft die Funktion, ist der Test rot; läuft sie durch, ist er grün. Mehr Vertrag gibt es nicht, und assert ist genau die Funktion, die im richtigen Moment wirft.

Am Ende steht eine Bilanz mit pass und fail, und darunter noch einmal jeder gescheiterte Test mit dem Vergleich, an dem es lag. Der Wert, den die Funktion wirklich geliefert hat, steht dabei, und das ist meistens schon die halbe Diagnose.

Beachte die letzte Zeile. Ein fehlgeschlagener Test setzt den Exit-Code auf 1, ein durchgelaufener auf 0. Das ist derselbe Exit-Code wie in Lektion 4.3, und er ist der Grund, warum Tests überhaupt etwas bewirken: Eine Pipeline liest keine Ausgabe, sie liest diese eine Zahl und bricht ab, wenn sie nicht null ist.

Wie du sie ausführst

Zwei Wege, und beide sind richtig.

node --test durchsucht das Projekt nach Dateien, die wie Tests heißen, und führt jede in einem eigenen Prozess aus. Als Test gilt dabei alles, was .test.js heißt oder in einem Verzeichnis test/ liegt. Das ist der übliche Weg, und er ist zugleich der Grund für die Namenskonvention: Du musst nirgends eintragen, welche Dateien Tests sind.

node meinedatei.test.js führt genau eine Datei aus, im selben Prozess. Die Ausgabe ist dieselbe. Das ist praktisch, solange du an einer einzelnen Datei arbeitest, und es ist der Weg, den die Aufgaben in diesem Abschnitt gehen.

Beide kannst du hier tippen. Hol die Aufgabe dieser Lektion nach vorn, dann liegt rabatt.test.js im Arbeitsverzeichnis, und im Terminal gehen beide Wege:

node rabatt.test.js

node --test

Bei einer einzigen Datei ist die Ausgabe bis auf die Zeitangaben dieselbe; die Gesamtdauer steigt beim zweiten Weg, weil er einen Kindprozess dazu startet. Der Unterschied zeigt sich erst ab zwei Dateien. Dann zählt die Bilanz über alle zusammen, und die Testnamen stehen flach untereinander, ohne dass dabeisteht, aus welcher Datei sie kommen.

Wo ein roter Test seine Zeile nennt

Unter der Bilanz kommt ein zweiter Block, failing tests:, und dort steht jeder rote Test noch einmal ausführlich. Er nennt seine Stelle zweimal, und die beiden meinen verschiedene Zeilen. Im ersten Beispiel dieser Lektion sind das:

  • test at drei-tests.js:16:1 zeigt auf die Zeile, in der test( steht, also auf den Test als Ganzes. Bei mehreren Dateien ist das die einzige Stelle, an der der Dateiname überhaupt auftaucht.
  • Die Zeile mit at TestContext.<anonymous> darunter zeigt auf die Behauptung, die nicht gestimmt hat, hier drei-tests.js:17:10. Das ist eine Fundstelle im Sinne von Lektion 1.4, also ein Knopf: Klick darauf, und der Editor steht auf dem assert.

Die obere ist keiner, denn sie trägt nur den Dateinamen und nicht den ganzen Pfad. Zum Springen nimmst du also die untere, und die ist ohnehin die interessantere: Der Test sagt dir, was nicht mehr stimmt, die Assert-Zeile, wo du nachsiehst.

Warum node:assert/strict

Es gibt zwei Einstiege, und du willst den zweiten.

assert mit und ohne strict
import lose from "node:assert";
import streng from "node:assert/strict";

function probiere(beschreibung, pruefung) {
  try {
    pruefung();
    console.log(`${beschreibung.padEnd(42)} besteht`);
  } catch (fehler) {
    console.log(`${beschreibung.padEnd(42)} scheitert`);
  }
}

probiere('assert.equal("3", 3)', () => lose.equal("3", 3));
probiere('assert/strict.equal("3", 3)', () => streng.equal("3", 3));
probiere("assert.equal(0, false)", () => lose.equal(0, false));
probiere("assert/strict.equal(0, false)", () => streng.equal(0, false));
probiere('assert.deepEqual({ a: 1 }, { a: "1" })', () => lose.deepEqual({ a: 1 }, { a: "1" }));
probiere('assert/strict.deepEqual({ a: 1 }, { a: "1" })', () => streng.deepEqual({ a: 1 }, { a: "1" }));

node:assert vergleicht mit ==. Damit ist die Zeichenkette "3" gleich der Zahl 3, die Null gleich false und ein Objekt mit dem Wert 1 gleich einem mit dem Wert "1". In einem Test ist das kein Komfort, sondern ein Loch: Genau diese Vergleiche sind es, an denen ein Programm später stolpert, und ein Test, der sie durchgehen lässt, sagt „alles in Ordnung”.

node:assert/strict vergleicht mit === und benutzt für deepEqual denselben strengen Maßstab. Die Funktionsnamen bleiben dieselben, du importierst nur einen anderen Pfad. Es gibt keinen guten Grund, in einem neuen Projekt den lockeren zu nehmen.

Die drei Funktionen, mit denen du weit kommst: assert.equal für einzelne Werte, assert.deepEqual für Objekte und Listen, und assert.throws für den Fall, dass etwas scheitern soll. Die dritte ist Lektion 17.3.

describe und it

Sobald ein Testbestand über zehn Fälle wächst, willst du ihn gliedern.

describe und it
import { describe, it } from "node:test";
import assert from "node:assert/strict";

function kuerze(text, laenge) {
  return text.length <= laenge ? text : `${text.slice(0, laenge - 1)}…`;
}

describe("kuerze", () => {
  it("laesst kurzen Text in Ruhe", () => {
    assert.equal(kuerze("Hallo", 10), "Hallo");
  });

  it("kuerzt langen Text und haengt Auslassungspunkte an", () => {
    assert.equal(kuerze("Programmieren mit Mario", 10), "Programmi…");
  });

  describe("Raender", () => {
    it("kommt mit leerem Text klar", () => {
      assert.equal(kuerze("", 5), "");
    });

    it("kuerzt auch, wenn der Text genau ein Zeichen zu lang ist", () => {
      assert.equal(kuerze("abcdef", 5), "abcd…");
    });
  });
});

describe gruppiert, it ist derselbe test unter anderem Namen. Verschachteln geht, und die Ausgabe rückt entsprechend ein. Am Ende zählt die Bilanz die Tests, nicht die Gruppen.

Der Nutzen ist nicht die Einrückung, sondern der Name. Wenn eine Gruppe kuerze heißt, müssen die Tests darin ihn nicht wiederholen, und aus kuerze kürzt langen Text wird beim Lesen ein Satz. Wie ein guter Testname aussieht, ist Lektion 17.2.

Wann doch ein Paket

Der eingebaute Läufer kann inzwischen das meiste: parallele Ausführung, Filter über --test-name-pattern, Beobachtungsmodus über --watch, Testabdeckung über --experimental-test-coverage und Attrappen über mock. Für ein Projekt dieser Größe gibt es schlicht nichts, wofür man etwas installieren müsste.

Zwei davon lohnt es, gleich einmal zu tippen. Beide hängen an der Aufgabe dieser Lektion, also an deinen vier Tests:

node --test --test-name-pattern="hundert"

Der Filter nimmt einen regulären Ausdruck und fährt nur die Tests, deren Name passt. Von den vier bleiben zwei übrig. Das ist der Handgriff für den Fall, dass ein Test rot ist und du nicht jedes Mal die ganze Datei sehen willst.

node --test --experimental-test-coverage

Darunter erscheint eine Tabelle: rabatt.js | 83.33 | 80.00 | 100.00 | 4-5. Die letzte Spalte ist die interessante, denn sie nennt die Zeilen, die beim Lauf nie ausgeführt wurden, und 4 und 5 sind der throw new TypeError, also die Prüfung auf den Preis. Deine vier Tests prüfen den Prozentsatz, und der Preis kommt bei keinem davon falsch herein. Die Zahl ist damit kein Zeugnis, sondern ein Hinweis auf einen Fall, den du noch nicht geschrieben hast. Wo die Zahl hoch ist, sagt sie dagegen erstaunlich wenig, und warum, steht in Lektion 17.5.

Der dritte, --watch, steht hier mit Absicht nicht dabei: Er braucht jemanden, der von außen in die Datei schreibt, und den gibt es in dieser Umgebung nicht. Lektion 18.3 sagt, warum.

Wofür man in größeren Projekten trotzdem zu Vitest oder Jest greift: Momentaufnahmen von Ausgabeschnipseln, ein reiches Ökosystem an Erweiterungen, eine Browser-Umgebung im selben Läufer, oder ganz einfach, weil das Projekt seit Jahren so gebaut ist. Nichts davon trifft hier zu, und das ist der beste Zeitpunkt, ohne Paket anzufangen: Wer die Bordmittel kennt, kann später beurteilen, was ein Werkzeug ihm wirklich abnimmt.

Zum Mitnehmen

Node bringt einen Testläufer mit. Kein Paket, keine Konfigurationsdatei, kein Aufbau: eine Datei, die test importiert, ist ein Testbestand, und der Exit-Code sagt, ob er durchgelaufen ist.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.