Abschnitt 17 · Lektion 3
Fehlerfälle testen
Der interessanteste Teil einer Funktion ist oft der, in dem sie sich weigert. In Lektion 16.1 hast du gelernt, welche Fehler behandelt gehören; jetzt schreibst du die Tests, die belegen, dass es passiert.
assert.throws braucht eine zweite Angabe
assert.throws(fn) prüft, dass fn wirft. Mehr nicht. Und „mehr nicht” ist hier das Problem.
import test from "node:test";
import assert from "node:assert/strict";
const PREISE = { 1: 4.99, 2: 12.5 };
function ladePreis(id) {
if (!Number.isInteger(id)) {
throw new TypeError("id muss eine ganze Zahl sein");
}
// Tippfehler: die Tabelle heisst PREISE. Das wirft einen ReferenceError.
return preise[id];
}
test("ohne zweite Angabe zaehlt sogar der Tippfehler als Erfolg", () => {
assert.throws(() => ladePreis(1));
});
test("ohne zweite Angabe ist auch der echte Fall gruen", () => {
assert.throws(() => ladePreis("x"));
});
test("mit TypeError faellt der Tippfehler auf", () => {
assert.throws(() => ladePreis(1), TypeError);
});
test("mit TypeError bleibt der echte Fall gruen", () => {
assert.throws(() => ladePreis("x"), TypeError);
}); In ladePreis steckt ein Tippfehler: Die Tabelle heißt PREISE, im Code steht preise. Jeder
Aufruf mit gültiger Kennung wirft deshalb einen ReferenceError.
Der erste Test heißt sinngemäß „mit einer gültigen Kennung geht etwas schief” und ist grün. Das ist absurd, und der Test hat es nicht gemerkt, weil er nur nach „irgendein Fehler” gefragt hat.
Erst mit TypeError als zweiter Angabe wird der Unterschied sichtbar: Der ReferenceError ist
keiner, der Test wird rot, und der Tippfehler kommt ans Licht.
Drei Formen für die zweite Angabe, alle drei nützlich:
- Eine Fehlerklasse, etwa
TypeErroroder deine eigene aus 16.1. Das ist der häufigste Fall. - Ein Objekt mit Eigenschaften, etwa
{ code: "keine_deckung" }. Geprüft wird jede Eigenschaft, die du hinschreibst, und nur die. - Eine Prüffunktion, die
trueliefern muss. Der Ausweg für alles, was komplizierter ist.
Was nicht dazugehört, ist der Meldungstext. Ein Test auf "Die Deckung reicht nicht" bricht,
sobald jemand das Komma ändert, und er ist nach einer Übersetzung wertlos. Prüf auf die Klasse oder
auf code, aus demselben Grund wie in Lektion 3.5 und 16.1: Der Code ist für das Programm, die
Meldung für den Menschen.
Der asynchrone Fall, und die Falle darin
Für abgelehnte Zusagen gibt es assert.rejects. Es sieht aus wie assert.throws und ist an einer
Stelle grundverschieden: Es liefert selbst eine Zusage.
import test from "node:test";
import assert from "node:assert/strict";
async function ladeKonto(id) {
if (id !== 1) throw new Error("gibt es nicht");
return { id, stand: 100 };
}
test("ohne await besteht der Test, obwohl nichts geprueft wurde", () => {
// ladeKonto(1) lehnt gar nicht ab, und trotzdem ist der Test gruen.
assert.rejects(() => ladeKonto(1), Error);
});
test("mit await faellt genau das auf", async () => {
await assert.rejects(() => ladeKonto(1), Error);
}); Beide Tests behaupten dasselbe, nämlich dass ladeKonto(1) ablehnt. Das tut es nicht, es liefert ein
Konto. Der erste Test ist trotzdem grün.
Der Grund ist derselbe wie in Lektion 16.2: Ohne await ist die Testfunktion fertig, bevor
assert.rejects zu einem Ergebnis kommt. Der Läufer sieht eine Funktion, die ohne Fehler
zurückgekehrt ist, und schreibt einen Haken daneben.
Das ist die gefährlichste Sorte Test, gefährlicher als gar keiner. Ein fehlender Test fällt irgendwann auf. Ein grüner Test, der nichts prüft, gibt dir Sicherheit, die du nicht hast, und niemand schaut sich einen grünen Test noch einmal an.
Die Regel dagegen ist kurz: Bei assert.rejects gehört ein await davor, und die Testfunktion
wird async. Dasselbe gilt für assert.doesNotReject und für jeden Aufruf im Test, der eine
Zusage liefert.
Attrappen mit mock
Manchmal will man einen Fehlerfall prüfen, den man nicht herstellen kann: Der Zahlungsdienst
antwortet nicht, die Platte ist voll, die Mail geht nicht raus. Dafür bringt node:test mock mit.
import { test, mock } from "node:test";
import assert from "node:assert/strict";
// Eine Abhaengigkeit, die im Test nicht wirklich laufen soll.
const dienst = {
schickeMail(an, text) {
throw new Error("wuerde wirklich eine Mail verschicken");
},
};
function bestellen(dienst, kunde) {
dienst.schickeMail(kunde, "Danke fuer deine Bestellung");
return { status: "angelegt" };
}
test("verschickt genau eine Mail an den Kunden", () => {
const attrappe = mock.method(dienst, "schickeMail", () => undefined);
const ergebnis = bestellen(dienst, "kundin@beispiel.de");
assert.equal(ergebnis.status, "angelegt");
assert.equal(attrappe.mock.callCount(), 1);
assert.deepEqual(attrappe.mock.calls[0].arguments, [
"kundin@beispiel.de",
"Danke fuer deine Bestellung",
]);
attrappe.mock.restore();
}); mock.method(objekt, "name", ersatz) tauscht eine Methode aus und zeichnet dabei auf, wie sie
aufgerufen wurde. Die Attrappe weiß danach, wie oft sie dran war (callCount()) und womit
(calls[0].arguments), und beides lässt sich prüfen.
Für einen Fehlerfall setzt du statt () => undefined einfach einen Ersatz, der wirft, und
schon lässt sich testen, was deine Funktion tut, wenn der Dienst nicht mitspielt.
restore() gibt das Original zurück. Wer es vergisst, verändert das Verhalten der folgenden Tests,
und dann ist man wieder bei dem geteilten Zustand aus Lektion 17.2. mock.reset() am Ende räumt
alles auf einmal auf.
Ein Wort der Vorsicht. Jede Attrappe ist eine Annahme darüber, wie sich das Original verhält, und diese Annahme prüft niemand. Wer zu viel ersetzt, testet am Ende sein eigenes Mockwerk. Die Faustregel: Attrappen für das, was nach draußen geht (Netz, Mail, Zahlung, Uhrzeit), und für alles andere die echte Sache.
Zum Mitnehmen
Ein Test, der nur prüft, dass irgendetwas schiefgeht, besteht auch bei einem Tippfehler. Geprüft wird deshalb, welcher Fehler kommt, und bei asynchronem Code entscheidet ein einziges await darüber, ob überhaupt etwas geprüft wird.
Jetzt du
Basis Konto, kostenlosZu 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.