Aufwärmen · Aufgabe 1 von 7
// B5.4 · ca. 32 Min. · Einstieg
node --test und der Watch-Modus
Nach dieser Lektion wählen Sie, welche Tests node --test ausführt, lesen seine Ausgabe und seinen Exit-Code, markieren Tests und lassen sie beim Bearbeiten erneut laufen.
Danach können Sie
- Die richtigen Testdateien ausführen: die Standardmuster, Dateinamen und Globs in Anführungszeichen
- Die Ausgabe lesen und wählen: spec, tap und dot, die Zusammenfassung, der Exit-Code und --test-name-pattern
- Tests mit skip, todo und only markieren, mit --watch erneut ausführen und die Befehle als npm-Skripte ablegen
Vorhersagen · Aufgabe 2 von 7
Sagen Sie es voraus, bevor Sie weiterlesen. Ein Ordner enthält diese fünf Dateien mit je einem Test: main.test.js, prices.spec.js, test/check.js, tests.js und notes_test.js. Wie viele Tests führt node --test ohne Dateinamen aus?
Üben · Aufgabe 3 von 7
Ergänzen Sie den Reporter, der ein Zeichen pro Test ausgibt: einen Punkt für jeden bestandenen Test und X für jeden fehlgeschlagenen.
node --test --test-reporter=____node --test --test-reporter=Üben · Aufgabe 4 von 7
Ordnen Sie jedem Befehl zu, was er für eine Datei mit den drei Tests 3.50 is 350 cents, 0.29 is 29 cents und a word is not a price ausgibt.
Üben · Aufgabe 5 von 7
node --test führt diese Datei aus. toCents('3,50') wirft einen RangeError, und toCents('-1') würde ebenfalls einen werfen. Was sagen die Zeilen pass und fail der Zusammenfassung, und wie lautet der Exit-Code?
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.skip('negative prices', () => { assert.equal(toCents('-1'), -100, "toCents('-1') should be -100"); }); test.todo('prices with a comma', () => { assert.equal(toCents('3,50'), 350, "toCents('3,50') should be 350"); });Denksport · Aufgabe 6 von 7
Knobelaufgabe. Ein Test dieser Datei hat {only: true}. Sie starten node --test ohne weiteres Flag. Welche Tests laufen?
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', {only: true}, () => { 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"); });Anwenden · Aufgabe 7 von 7
Mini-Aufgabe. Nehmen Sie main.js und main.test.js aus B5.3 (toCents und seine Tests). Tragen Sie in package.json "test": "node --test" und "test:watch": "node --test --watch" ein. Starten Sie npm test, dann npm test -- --test-reporter=dot und npm test -- --test-name-pattern=cents. Starten Sie npm run test:watch, ändern Sie in main.js Math.round in Math.floor und speichern Sie; lesen Sie den Fehlschlag, ändern Sie es zurück, speichern Sie erneut und beenden Sie mit Strg+C.
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
Eine Testdatei, vier Arten, sie auszuführen
prices.test.js hat fünf Tests: drei gewöhnliche, einen übersprungenen und einen TODO-Test, der noch keine Funktion hat. main.js startet node --test viermal als Kindprozess und gibt aus, was jeder Lauf bis zu seiner ersten Leerzeile zeigt, ohne die Dauern, die sich bei jedem Lauf ändern. Die Läufe: der Standard-Reporter spec, ein Namensmuster, der Reporter dot und der Reporter tap für einen Test.
main.js
import {spawnSync} from 'node:child_process';
// Runs node with these arguments and prints what the terminal shows, up to the
// first blank line, without durations such as (1.23ms) and a few summary lines.
function run(args) {
console.log('$ node ' + args.join(' '));
const {status, stdout} = spawnSync(process.execPath, args, {encoding: 'utf8'});
const shown = stdout.split('\n\n')[0].replace(/ \(\d+(\.\d+)?ms\)/g, '');
console.log(shown.split('\n').filter((line) => !/suites|cancelled|duration_ms/.test(line)).join('\n').trimEnd());
console.log('exit code ' + status + '\n');
}
run(['--test']);
run(['--test', '--test-name-pattern=cents']);
run(['--test', '--test-reporter=dot']);
run(['--test', '--test-reporter=tap', '--test-name-pattern=word']);
prices.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.round(Number(text) * 100);
}
prices.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {toCents} from './prices.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.skip('negative prices', () => {
assert.equal(toCents('-1'), -100, "toCents('-1') should be -100");
});
test.todo('prices with a comma');
test('a word is not a price', () => {
assert.throws(() => toCents('abc'), RangeError, "toCents('abc') should throw a RangeError");
});
Ausführen mit
node main.jsAusgabe
$ node --test
✔ 3.50 is 350 cents
✔ 0.29 is 29 cents
﹣ negative prices # SKIP
✔ prices with a comma # TODO
✔ a word is not a price
ℹ tests 5
ℹ pass 3
ℹ fail 0
ℹ skipped 1
ℹ todo 1
exit code 0
$ node --test --test-name-pattern=cents
✔ 3.50 is 350 cents
✔ 0.29 is 29 cents
ℹ tests 2
ℹ pass 2
ℹ fail 0
ℹ skipped 0
ℹ todo 0
exit code 0
$ node --test --test-reporter=dot
.....
exit code 0
$ node --test --test-reporter=tap --test-name-pattern=word
TAP version 13
# Subtest: a word is not a price
ok 1 - a word is not a price
---
type: 'test'
...
1..1
# tests 1
# pass 1
# fail 0
# skipped 0
# todo 0
exit code 0- node --test fand prices.test.js an seinem Namen; main.js und prices.js sind keine Testdateien.
- Der übersprungene und der TODO-Test werden getrennt von pass und fail gezählt.
- Das Namensmuster führte zwei Tests aus und ließ die anderen drei in der Ausgabe weg.
- describe und it, Hooks, Mocks und Coverage sind weitere Funktionen von node:test; die Stufe Intermediate behandelt sie.
Übungen
Übung 1 von 2
Welche Dateien sind Testdateien?
Schreiben Sie isTestFile(path) für Pfade wie 'src/prices.test.js', mit / zwischen den Ordnern. Die Funktion gibt true zurück, wenn node --test ohne Dateinamen die Datei ausführen würde. Die Endung muss js, cjs, mjs, ts, cts oder mts sein. Dann muss der Name davor test sein, mit test- beginnen oder auf .test, -test oder _test enden; oder einer der Ordner heißt genau test. main.test.js und test/unit/f.js zählen also, latest.js und contest/x.js nicht.
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
Hinweis 1
Zerlegen Sie den Pfad: const parts = path.split('/'); der Dateiname ist parts.at(-1), die Ordner sind parts.slice(0, -1).
Hinweis 2
Prüfen Sie zuerst die Endung mit /^(.+)\.(js|cjs|mjs|ts|cts|mts)$/; die erste Gruppe ist der Name vor der Endung.
Hinweis 3
Dann den Namen: === 'test', startsWith('test-') oder /[.\-_]test$/ für die drei Endungen. latest endet auf test, aber ohne Trennzeichen davor.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// True when node --test, run without file names, would run this file.
// path uses / between folders, such as 'src/prices.test.js'.
export function isTestFile(path) {
const parts = path.split('/');
const match = /^(.+)\.(js|cjs|mjs|ts|cts|mts)$/.exec(parts.at(-1));
if (!match) return false;
if (parts.slice(0, -1).includes('test')) return true; // anywhere inside a folder named test
const stem = match[1];
return stem === 'test' || stem.startsWith('test-') || /[.\-_]test$/.test(stem);
}
for (const path of ['main.test.js', 'b-test.js', 'test/e.js', 'main.js', 'prices.spec.js']) {
console.log(path, isTestFile(path));
}
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
// True when node --test, run without file names, would run this file.
// path uses / between folders, such as 'src/prices.test.js'.
export function isTestFile(path) {
return path.endsWith('.test.js');
}
for (const path of ['main.test.js', 'b-test.js', 'test/e.js', 'main.js', 'prices.spec.js']) {
console.log(path, isTestFile(path));
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {isTestFile} from './main.js';
test('Namen, die auf .test, -test oder _test enden, zählen', () => {
for (const path of ['main.test.js', 'src/prices.test.mjs', 'b-test.js', 'c_test.cjs', 'lib/g.test.ts', 'lib/my-test.ts']) {
assert.equal(isTestFile(path), true, `isTestFile('${path}') sollte true sein`);
}
});
test('test.js und Namen, die mit test- beginnen, zählen', () => {
for (const path of ['test.js', 'src/test.mjs', 'test-d.js']) {
assert.equal(isTestFile(path), true, `isTestFile('${path}') sollte true sein`);
}
});
test('jedes Skript in einem Ordner namens test zählt', () => {
for (const path of ['test/e.js', 'test/unit/f.js']) {
assert.equal(isTestFile(path), true, `isTestFile('${path}') sollte true sein`);
}
});
test('andere Namen zählen nicht', () => {
for (const path of ['main.js', 'prices.spec.js', 'tests.js', 'latest.js', 'testing.js', 'contest/x.js']) {
assert.equal(isTestFile(path), false, `isTestFile('${path}') sollte false sein`);
}
});
test('nur Skript-Endungen zählen, auch in einem Ordner namens test', () => {
assert.equal(isTestFile('test/data.json'), false, "isTestFile('test/data.json') sollte false sein");
assert.equal(isTestFile('main.test.txt'), false, "isTestFile('main.test.txt') sollte false sein");
});
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.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenÜbung 2 von 2
Zählen wie die Zusammenfassung
Schreiben Sie summarize(results). Jedes Ergebnis ist {name, ok}, dazu skip: true oder todo: true, wenn der Test so markiert war. Geben Sie {tests, pass, fail, skipped, todo, exitCode} zurück. Ein übersprungener Test zählt nur unter skipped, ein TODO-Test nur unter todo, ob er bestand oder nicht. Die übrigen zählen unter pass oder fail. exitCode ist 1, wenn fail größer als 0 ist, sonst 0. tests zählt jedes Ergebnis.
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
Hinweis 1
Zählen Sie zuerst skipped und todo mit filter: results.filter((r) => r.skip).length.
Hinweis 2
Die gewöhnlichen Tests sind die ohne Markierung: results.filter((r) => !r.skip && !r.todo). Teilen Sie sie nach ok auf.
Hinweis 3
exitCode hängt nur von den gewöhnlichen Fehlschlägen ab: fail > 0 ? 1 : 0.
Eine Lösung zeigen
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
// Counts results the way node --test does in its summary.
// Each result is {name, ok}, with skip: true or todo: true when the test was marked so.
export function summarize(results) {
const skipped = results.filter((r) => r.skip).length;
const todo = results.filter((r) => r.todo && !r.skip).length;
const plain = results.filter((r) => !r.skip && !r.todo);
const pass = plain.filter((r) => r.ok).length;
const fail = plain.length - pass;
return {tests: results.length, pass, fail, skipped, todo, exitCode: fail > 0 ? 1 : 0};
}
console.log(summarize([
{name: '3.50 is 350 cents', ok: true},
{name: 'negative prices', ok: true, skip: true},
{name: 'prices with a comma', ok: false, todo: true}
]));
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
// Counts results the way node --test does in its summary.
// Each result is {name, ok}, with skip: true or todo: true when the test was marked so.
export function summarize(results) {
const pass = results.filter((r) => r.ok).length;
return {tests: results.length, pass, fail: results.length - pass, skipped: 0, todo: 0, exitCode: 0};
}
console.log(summarize([
{name: '3.50 is 350 cents', ok: true},
{name: 'negative prices', ok: true, skip: true},
{name: 'prices with a comma', ok: false, todo: true}
]));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {summarize} from './main.js';
test('gewöhnliche Tests zählen als pass oder fail', () => {
const got = summarize([{name: 'a', ok: true}, {name: 'b', ok: false}, {name: 'c', ok: true}]);
assert.deepEqual(got, {tests: 3, pass: 2, fail: 1, skipped: 0, todo: 0, exitCode: 1}, `summarize ergab ${JSON.stringify(got)}`);
});
test('übersprungene Tests zählen nur als skipped', () => {
const got = summarize([{name: 'a', ok: true}, {name: 'b', ok: true, skip: true}]);
assert.deepEqual(got, {tests: 2, pass: 1, fail: 0, skipped: 1, todo: 0, exitCode: 0}, `summarize ergab ${JSON.stringify(got)}`);
});
test('ein fehlschlagender TODO-Test lässt den Lauf nicht fehlschlagen', () => {
const got = summarize([{name: 'a', ok: true}, {name: 'b', ok: false, todo: true}, {name: 'c', ok: true, todo: true}]);
assert.deepEqual(got, {tests: 3, pass: 1, fail: 0, skipped: 0, todo: 2, exitCode: 0}, `summarize ergab ${JSON.stringify(got)}`);
});
test('keine Ergebnisse: alles 0, Exit-Code 0', () => {
const got = summarize([]);
assert.deepEqual(got, {tests: 0, pass: 0, fail: 0, skipped: 0, todo: 0, exitCode: 0}, `summarize([]) ergab ${JSON.stringify(got)}`);
});
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.jsPrüfungen ausführen (learnrun.js muss im selben Ordner liegen):
node --testlearnrun.js herunterladenHäufige Fehler
Die Optionen hinter die Funktion setzen
import {test} from 'node:test';
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);
}
test('negative prices', () => {
assert.equal(toCents('-1'), -100, "toCents('-1') should be -100");
}, {skip: true});
Was Node.js ausgibt
RangeError: not a price: -1Warum, und die Lösung
Die Reihenfolge ist test(name, options, fn). Mit {skip: true} hinter der Funktion ignoriert node:test die Optionen: Der Test läuft, toCents('-1') wirft, und der Test schlägt fehl. Setzen Sie die Optionen zwischen Namen und Funktion, test('negative prices', {skip: true}, () => { … }), oder schreiben Sie test.skip('negative prices', () => { … }), was dasselbe bedeutet.
t.skip() aufrufen, ohne t zu übernehmen
import {test} from 'node:test';
test('prices with a comma', () => {
t.skip('commas come later');
});
Was Node.js ausgibt
ReferenceError: t is not definedWarum, und die Lösung
Die Beispiele der Dokumentation schreiben test('…', (t) => { t.skip(); }): t ist der Testkontext, der erste Parameter der Testfunktion. Kopieren Sie den Aufruf ohne den Parameter, ist t ein unbekannter Name, und der Test schlägt fehl, statt übersprungen zu werden. Um einen ganzen Test zu überspringen, braucht test.skip('prices with a comma', () => { … }) gar kein t.
describe und it verwenden, ohne sie zu importieren
import assert from 'node:assert/strict';
describe('toCents', () => {
it('3.50 is 350 cents', () => {
assert.equal(Math.round(3.5 * 100), 350, '3.50 should be 350 cents');
});
});
Was Node.js ausgibt
ReferenceError: describe is not definedWarum, und die Lösung
Andere Testwerkzeuge stellen describe und it als globale Namen bereit; in Node.js kommen sie aus node:test, wie test. Schreiben Sie import {describe, it} from 'node:test'; oder bleiben Sie bei test(), wie diese Stufe es tut. Die Stufe Intermediate behandelt describe, it und Hooks wie beforeEach.
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.