Zum Inhalt springen
aviral gupta

// B5.3 · ca. 31 Min. · Einstieg

Tests mit node:test und node:assert

Nach dieser Lektion können Sie Tests für die Funktionen schreiben, die Ihre main.js exportiert, die passende Assertion aus node:assert/strict wählen und lesen, was eine fehlgeschlagene Assertion Ihnen sagt.

Lektion 3 von 5 in B5 Ereignisse, Eingaben und erste Tests

Danach können Sie

  • Synchrone und asynchrone Tests mit test() für eine aus main.js exportierte Funktion schreiben und mit node --test ausführen
  • Die Assertion wählen: equal oder deepEqual, ok, match sowie throws oder rejects mit einem erwarteten Fehler
  • Eine fehlgeschlagene Assertion lesen: ihre Meldung, den tatsächlichen und den erwarteten Wert
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus B4.2: Eine async-Funktion wirft. Was gibt das Programm aus?

    async function load() {
      throw new Error('no file');
    }
    
    const result = load();
    console.log(result instanceof Promise);
    result.catch((error) => console.log('rejected:', error.message));
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie es voraus, bevor Sie weiterlesen. node --test führt diese Datei aus. Was steht in der Zusammenfassung?

    import {test} from 'node:test';
    
    test('returns false', () => false);
    
    test('compares 1 + 1 with 3', () => {
      1 + 1 === 3;
    });
    
    test('throws', () => {
      throw new Error('boom');
    });
  3. Üben · Aufgabe 3 von 7

    tally liefert ein neues Objekt mit Zählungen. Ergänzen Sie die Assertion, die es mit dem erwarteten Objekt vergleicht.

    assert.____(tally(['tea', 'milk', 'tea']), {tea: 2, milk: 1}, 'tally should count each word');
    assert.(tally(['tea', 'milk', 'tea']), {tea: 2, milk: 1}, 'tally should count each word');
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jeder Assertion zu, was sie prüft.

  5. Üben · Aufgabe 5 von 7

    Lesen Sie die echte Ausgabe von node --test für diese main.js. Welchen Wert hat toCents('0.29') geliefert?

    // main.js
    // Turns a price like '3.50' into whole cents.
    export function toCents(text) {
      if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
      return Math.floor(Number(text) * 100);
    }
    
    // main.test.js
    import {test} from 'node:test';
    import assert from 'node:assert/strict';
    import {toCents} from './main.js';
    
    test('3.50 is 350 cents', () => {
      assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
    });
    
    test('0.29 is 29 cents', () => {
      assert.equal(toCents('0.29'), 29, "toCents('0.29') should be 29");
    });
    
    test('a word is not a price', () => {
      assert.throws(() => toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
    });
    
    // $ node --test   (durations, blank lines and the stack trace left out)
    // ✔ 3.50 is 350 cents
    // ✖ 0.29 is 29 cents
    // ✔ a word is not a price
    // ℹ tests 3
    // ℹ pass 2
    // ℹ fail 1
    // ✖ failing tests:
    // test at main.test.js:9:1
    // ✖ 0.29 is 29 cents
    //   AssertionError [ERR_ASSERTION]: toCents('0.29') should be 29
    //   28 !== 29
    //     actual: 28,
    //     expected: 29,
    //     operator: 'strictEqual',
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Vier Assertions, jede für sich ausgeführt. Wie viele schlagen fehl?

    import assert from 'node:assert/strict';
    
    const results = [];
    const check = (fn) => {
      try {
        fn();
        results.push('pass');
      } catch {
        results.push('FAIL');
      }
    };
    check(() => assert.equal(NaN, NaN, 'NaN'));
    check(() => assert.equal(0, -0, 'zero'));
    check(() => assert.deepEqual([1], [1], 'deep'));
    check(() => assert.equal([1], [1], 'same array?'));
    console.log(results.filter((r) => r === 'FAIL').length);
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe. Schreiben Sie in einem neuen Ordner mit "type": "module" in package.json eine main.js, die countWords(text) und ein asynchrones countFileWords(path) exportiert, das eine Datei mit node:fs/promises liest (B3.2). Schreiben Sie main.test.js mit vier Tests: Wörter, getrennt durch beliebigen Leerraum, ein Text nur aus Leerzeichen, eine Datei words.txt mit 4 Wörtern und eine fehlende Datei, die mit dem Code ENOENT ablehnt. Starten Sie node --test. Bauen Sie dann absichtlich einen Fehler in countWords ein und lesen Sie den Fehlschlag.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Was test() tut, von Hand

Bevor node --test eine ganze Datei ausführt, sehen Sie, was es für einen Test tut. check(name, fn) ruft fn auf und meldet pass oder FAIL mit dem Fehler, so wie test() ein throw als Fehlschlag zählt. Die Assertions darin sind die echten aus node:assert/strict: equal, deepEqual, throws mit einer Klasse und mit einem Objekt, ok und match. Eine Prüfung schlägt absichtlich fehl: equal auf zwei Objekten.

main.js

import assert from 'node:assert/strict';

// Turns a price like '3.50' into whole cents.
export function toCents(text) {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
}

// What test() does for you: run fn, and count a throw as a failure.
function check(name, fn) {
  try {
    fn();
    console.log('pass', name);
  } catch (error) {
    console.log('FAIL', name, '->', error.name + ':', error.message.split('\n')[0]);
  }
}

check('3.50 is 350 cents', () => {
  assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
});
check('0.29 is 29 cents, not 28', () => {
  assert.equal(toCents('0.29'), 29, "toCents('0.29') should be 29");
});
check('every price in a list', () => {
  assert.deepEqual(['1', '2.5'].map(toCents), [100, 250], 'each price in cents');
});
check('equal on two objects', () => {
  assert.equal({cents: 100}, {cents: 100}, 'two object literals are two objects');
});
check('a word is not a price', () => {
  assert.throws(() => toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
});
check('the error names the input', () => {
  assert.throws(() => toCents('3,50'), {name: 'RangeError', message: /3,50/}, 'the message should contain 3,50');
});
check('ok and match', () => {
  assert.ok(toCents('0') === 0, "toCents('0') should be 0");
  assert.match('not a price: x', /^not a price/, 'the message should start with not a price');
});

Ausführen mit

node main.js

Ausgabe

pass 3.50 is 350 cents
pass 0.29 is 29 cents, not 28
pass every price in a list
FAIL equal on two objects -> AssertionError: two object literals are two objects
pass a word is not a price
pass the error names the input
pass ok and match
  • Jede fehlgeschlagene Assertion warf einen AssertionError, dessen Meldung mit dem letzten Argument begann.
  • equal schlug bei zwei Objekten mit gleichem Inhalt fehl; deepEqual bestand bei den Arrays.
  • throws bekam eine Funktion, () => toCents('abc'), konnte sie also selbst aufrufen und den Fehler fangen.
  • Die fehlgeschlagene Prüfung hielt die anderen nicht auf: Jede Prüfung fängt ihren eigenen Fehler, wie jeder Test.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 2

Assertions, die einen Fehler fangen

Schreiben Sie die Assertions eines Tests als Funktion: checkToCents(toCents) muss einen AssertionError werfen, wenn das übergebene toCents eine Regel bricht, und nichts tun, wenn es korrekt ist. Die Regeln: toCents('7') ist 700, toCents('3.50') ist 350, toCents('0.29') ist 29, alles Zahlen, und toCents('abc') wirft einen RangeError. Die Tests prüfen Ihre Funktion mit einem korrekten toCents und mit kaputten. Geben Sie jeder Assertion eine Meldung.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Eine Assertion pro Regel: assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350"); equal ist strikt, also fällt '350' durch.

  2. Hinweis 2

    Dasselbe für '0.29' und 29: Diese Assertion fängt Math.floor.

  3. Hinweis 3

    Für abc: assert.throws(() => toCents('abc'), RangeError, '…'). Übergeben Sie eine Funktion; mit RangeError als Klasse fällt auch ein TypeError durch.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import assert from 'node:assert/strict';

// Throws an AssertionError when toCents breaks one of its rules.
export function checkToCents(toCents) {
  assert.equal(toCents('7'), 700, "toCents('7') should be 700");
  assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
  assert.equal(toCents('0.29'), 29, "toCents('0.29') should be 29");
  assert.throws(() => toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
}

// A correct toCents passes the check.
checkToCents((text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
});
console.log('the correct toCents passed');
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

import assert from 'node:assert/strict';

// Throws an AssertionError when toCents breaks one of its rules.
export function checkToCents(toCents) {
  assert.equal(toCents('7'), 700, "toCents('7') should be 700");
  // Add the other rules: '3.50', '0.29' and 'abc'.
}

// A correct toCents passes the check.
checkToCents((text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
});
console.log('the correct toCents passed');

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {checkToCents} from './main.js';

// A correct toCents, and broken ones with one bug each.
const good = (text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
};
const floors = (text) => (good(text), Math.floor(Number(text) * 100));
const strings = (text) => String(good(text));
const noThrow = (text) => (/^\d+(\.\d{1,2})?$/.test(text) ? good(text) : NaN);
const wrongError = (text) => {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new TypeError('not a price: ' + text);
  return good(text);
};
const catches = (broken) => {
  try {
    checkToCents(broken);
  } catch (error) {
    return error.name;
  }
  return 'nothing';
};

test('Ihre Prüfung akzeptiert ein korrektes toCents', () => {
  checkToCents(good);
});

test('Ihre Prüfung fängt Math.floor, das aus 0.29 28 macht', () => {
  assert.equal(catches(floors), 'AssertionError', 'checkToCents(floors) sollte einen AssertionError werfen');
});

test('Ihre Prüfung fängt ein toCents, das Strings liefert', () => {
  assert.equal(catches(strings), 'AssertionError', 'checkToCents(strings) sollte einen AssertionError werfen');
});

test('Ihre Prüfung fängt ein toCents, das NaN liefert, statt zu werfen', () => {
  assert.equal(catches(noThrow), 'AssertionError', 'checkToCents(noThrow) sollte einen AssertionError werfen');
});

test('Ihre Prüfung fängt ein toCents, das einen TypeError wirft', () => {
  assert.equal(catches(wrongError), 'AssertionError', 'checkToCents(wrongError) sollte einen AssertionError werfen');
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Übung 2 von 2

Assertions für eine async-Funktion

Jetzt asynchron: checkFindUser(findUser) muss mit einem AssertionError ablehnen, wenn findUser eine Regel bricht, und sich erfüllen, wenn es korrekt ist. Die Regeln: await findUser(1) ergibt genau {id: 1, name: 'Ada'}, await findUser(2) genau {id: 2, name: 'Lin'}, ohne weitere Felder, und findUser(99) lehnt mit einem Fehler mit der Meldung no user 99 ab. Der Startcode prüft nur einen Namen. Geben Sie jeder Assertion eine Meldung.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Vergleichen Sie ganze Objekte mit deepEqual: assert.deepEqual(await findUser(1), {id: 1, name: 'Ada'}, '…'). Es ist strikt, also ist '1' nicht 1, und ein zusätzliches Feld fällt durch.

  2. Hinweis 2

    Für 99: await assert.rejects(findUser(99), {message: 'no user 99'}, '…'). Ohne await wäre die Prüfung fertig, bevor das Promise ablehnt.

  3. Hinweis 3

    checkFindUser ist async, also lehnt eine fehlgeschlagene Assertion darin sein Promise ab: Genau diesen Fehlschlag suchen die Tests.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import assert from 'node:assert/strict';

// Rejects with an AssertionError when findUser breaks one of its rules.
export async function checkFindUser(findUser) {
  assert.deepEqual(await findUser(1), {id: 1, name: 'Ada'}, 'findUser(1) should give {id: 1, name: Ada}');
  assert.deepEqual(await findUser(2), {id: 2, name: 'Lin'}, 'findUser(2) should give {id: 2, name: Lin}');
  await assert.rejects(findUser(99), {message: 'no user 99'}, 'findUser(99) should reject with no user 99');
}

// A correct findUser passes the check.
const users = {1: 'Ada', 2: 'Lin'};
await checkFindUser(async (id) => {
  if (!(id in users)) throw new Error('no user ' + id);
  return {id, name: users[id]};
});
console.log('the correct findUser passed');
Auf dem eigenen Computer ausführen

Installieren Sie Node.js 24 LTS oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

import assert from 'node:assert/strict';

// Rejects with an AssertionError when findUser breaks one of its rules.
export async function checkFindUser(findUser) {
  const user = await findUser(1);
  assert.equal(user.name, 'Ada', 'findUser(1) should be called Ada');
}

// A correct findUser passes the check.
const users = {1: 'Ada', 2: 'Lin'};
await checkFindUser(async (id) => {
  if (!(id in users)) throw new Error('no user ' + id);
  return {id, name: users[id]};
});
console.log('the correct findUser passed');

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {checkFindUser} from './main.js';

// A correct findUser, and broken ones with one bug each.
const users = {1: 'Ada', 2: 'Lin'};
const good = async (id) => {
  if (!(id in users)) throw new Error('no user ' + id);
  return {id, name: users[id]};
};
const stringId = async (id) => ({...(await good(id)), id: String(id)});
const extraField = async (id) => ({...(await good(id)), admin: false});
const noReject = async (id) => (id in users ? good(id) : undefined);
const otherMessage = async (id) => {
  if (!(id in users)) throw new Error('not found');
  return good(id);
};
const catches = async (broken) => {
  try {
    await checkFindUser(broken);
  } catch (error) {
    return error.name;
  }
  return 'nothing';
};

test('Ihre Prüfung akzeptiert ein korrektes findUser', async () => {
  await checkFindUser(good);
});

test('Ihre Prüfung fängt eine id, die ein String ist', async () => {
  assert.equal(await catches(stringId), 'AssertionError', 'checkFindUser(stringId) sollte mit einem AssertionError ablehnen');
});

test('Ihre Prüfung fängt ein zusätzliches Feld', async () => {
  assert.equal(await catches(extraField), 'AssertionError', 'checkFindUser(extraField) sollte mit einem AssertionError ablehnen');
});

test('Ihre Prüfung fängt eine unbekannte id, die nicht ablehnt', async () => {
  assert.equal(await catches(noReject), 'AssertionError', 'checkFindUser(noReject) sollte mit einem AssertionError ablehnen');
});

test('Ihre Prüfung fängt die falsche Fehlermeldung', async () => {
  assert.equal(await catches(otherMessage), 'AssertionError', 'checkFindUser(otherMessage) sollte mit einem AssertionError ablehnen');
});

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

node main.js

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

node --test
learnrun.js herunterladen

Häufige Fehler

test benutzen, ohne es zu importieren

import assert from 'node:assert/strict';

function toCents(text) {
  return Math.round(Number(text) * 100);
}

test('3.50 is 350 cents', () => {
  assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
});

Was Node.js ausgibt

ReferenceError: test is not defined

Warum, und die Lösung

Manche anderen Test-Werkzeuge machen test global. In Node.js nicht: test stammt aus dem Modul node:test, und nur mit dem Präfix node:. Fügen Sie oben neben dem assert-Import import {test} from 'node:test'; hinzu. Für assert gilt dasselbe: Importieren Sie es aus 'node:assert/strict'.

Die Funktion in assert.throws aufrufen

import assert from 'node:assert/strict';

function toCents(text) {
  if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new RangeError('not a price: ' + text);
  return Math.round(Number(text) * 100);
}

assert.throws(toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
console.log('all checks passed');

Was Node.js ausgibt

RangeError: not a price: abc

Warum, und die Lösung

assert.throws(toCents('abc'), …) ruft toCents auf, bevor throws überhaupt beginnt, also fliegt der RangeError aus dieser Zeile, und das Programm stürzt ab. throws muss die Funktion selbst aufrufen, um den Fehler zu fangen: Übergeben Sie eine Funktion, assert.throws(() => toCents('abc'), RangeError, '…'). Dasselbe gilt für assert.rejects mit einer async-Funktion, oder Sie übergeben ihm das Promise und warten mit await.

Eine Funktion testen, die nicht exportiert ist

import assert from 'node:assert/strict';
import {toCents} from './prices.js';

assert.equal(toCents('3.50'), 350, "toCents('3.50') should be 350");
console.log('all checks passed');

Was Node.js ausgibt

SyntaxError: The requested module './prices.js' does not provide an export named 'toCents'

Warum, und die Lösung

Eine Testdatei kann wie jedes Modul nur importieren, was das andere Modul exportiert. Hier deklariert prices.js toCents, exportiert es aber nicht, also scheitert der Import, bevor ein einziger Test läuft; node --test meldet dann die ganze Datei als fehlgeschlagen. Schreiben Sie export function toCents(text) in das Modul, das Sie testen.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Test ist eine Funktion, die wirft, wenn etwas nicht stimmt

Importieren Sie test aus 'node:test' (nur mit dem Präfix node:) und schreiben Sie test('3.50 is 350 cents', () => { … }) in eine Datei wie main.test.js. Ein Test schlägt fehl, wenn seine Funktion wirft oder, bei einer async-Funktion, wenn ihr Promise ablehnt; sonst besteht er. false zurückzugeben oder einen Vergleich hinzuschreiben, den niemand prüft, lässt keinen Test fehlschlagen: Eine Assertion muss werfen. Tests importieren die Funktionen, die main.js exportiert. node --test ohne weitere Argumente findet solche Testdateien, führt sie aus, gibt pro Test ✔ oder ✖ und eine Zusammenfassung aus und endet mit Exit-Code 1, wenn ein Test fehlschlug. Installieren müssen Sie nichts.

Wählen Sie die Assertion, die sagt, was Sie meinen

Importieren Sie assert aus 'node:assert/strict'; im strikten Modus bedeutet equal dasselbe wie strictEqual. equal vergleicht mit Object.is: richtig für Zahlen und Strings, doch zwei Objekte oder Arrays sind nur gleich, wenn es dasselbe Objekt ist. deepEqual vergleicht Struktur und Inhalt, strikt: {id: 1} ist nicht {id: '1'}. ok prüft, ob ein Wert truthy ist, match, ob ein String zu einem regulären Ausdruck passt. throws(fn, expected) ruft fn auf und erwartet einen Fehler; übergeben Sie die Funktion, rufen Sie sie nicht auf. expected ist eine Klasse, ein RegExp, der gegen den Fehler als Text geprüft wird, oder ein Objekt wie {message: /abc/}. Für Promises: await assert.rejects(promise, expected).

Den Fehlschlag lesen

Eine fehlgeschlagene Assertion wirft einen AssertionError. Seine Meldung beginnt mit dem letzten Argument, das Sie übergeben haben (Node.js 24 setzt den Vergleich darunter); ohne eines schreibt Node.js eine Standardmeldung wie Expected values to be strictly equal:. node --test gibt für jeden fehlgeschlagenen Test Datei und Zeile aus, die Zeile AssertionError [ERR_ASSERTION]: mit Ihrer Meldung, einen kurzen Vergleich wie 28 !== 29 (der tatsächliche Wert zuerst) und die Eigenschaften actual und expected. Schreiben Sie also Meldungen, die sagen, was hätte passieren sollen, etwa toCents('0.29') should be 29, und die Ausgabe sagt Ihnen den Rest. Die Assertions eines Tests laufen der Reihe nach; die erste, die fehlschlägt, beendet den Test.

Quellen

Zuletzt geprüft am 4. Oktober 2026