Abschnitt 17 · Lektion 2
Funktionen testen
Einen Test zu schreiben ist leicht. Testfälle zu wählen, die etwas aussagen, ist die eigentliche Arbeit, und hier gibt es zum Glück ein Rezept.
Drei Sorten Fälle, jedes Mal
Der Normalfall. Das, wofür die Funktion gebaut ist, mit gewöhnlichen Werten.
Die Ränder. Leer, eins, viele. Null, negativ, sehr groß. Der erste und der letzte Eintrag. Genau eins zu wenig und genau eins zu viel. Dort sitzen die Fehler, weil dort die Bedingungen umschlagen.
Der Fall, der schiefgehen soll. Ungültige Eingabe, fehlendes Feld, verbotener Zustand. Ohne ihn weißt du nur, dass deine Funktion arbeitet, wenn alle lieb sind. Das ist Lektion 17.3.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
// Der Median: der mittlere Wert einer sortierten Liste.
function median(zahlen) {
const sortiert = [...zahlen].sort((a, b) => a - b);
return sortiert[Math.floor(sortiert.length / 2)];
}
describe("nur der Normalfall", () => {
it("findet den mittleren Wert", () => {
assert.equal(median([1, 2, 3]), 2);
});
});
describe("dazu die Raender", () => {
it("kommt mit einem einzigen Wert klar", () => {
assert.equal(median([7]), 7);
});
it("sortiert vorher, die Eingabe darf unsortiert sein", () => {
assert.equal(median([9, 1, 5]), 5);
});
it("mittelt bei gerader Anzahl die beiden mittleren", () => {
assert.equal(median([1, 2, 3, 4]), 2.5);
});
}); Der Median einer ungeraden Liste ist der mittlere Wert, und genau das prüft der erste Test. Er ist grün, und die Funktion ist trotzdem kaputt: Bei gerader Anzahl gehört der Mittelwert der beiden mittleren Werte heraus, und der letzte Test findet das.
Daran siehst du den Wert eines Tests. Er liegt nicht darin, dass er grün ist, sondern darin, welche Fehler er finden könnte. Ein Test, der nur den Normalfall prüft, könnte nur einen Fehler finden, bei dem gar nichts mehr funktioniert, und der wäre ohnehin sofort aufgefallen.
Vorbereiten, ausführen, prüfen
Ein Test hat drei Abschnitte, und es lohnt sich, sie sichtbar zu lassen.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
function neueListe() {
const eintraege = [];
return {
anlegen: (text) => eintraege.push({ id: eintraege.length + 1, text }),
alle: () => [...eintraege],
};
}
describe("ein Zustand fuer alle Tests", () => {
// Sieht sparsam aus und ist der haeufigste Anfaengerfehler.
const liste = neueListe();
it("legt die erste Notiz an", () => {
liste.anlegen("Milch kaufen");
assert.equal(liste.alle().length, 1);
});
it("legt eine Notiz mit der Kennung 1 an", () => {
liste.anlegen("Brot holen");
assert.equal(liste.alle()[0].id, 1);
assert.equal(liste.alle().length, 1);
});
});
describe("jeder Test bereitet selbst vor", () => {
it("legt die erste Notiz an", () => {
// vorbereiten
const liste = neueListe();
// ausfuehren
liste.anlegen("Milch kaufen");
// pruefen
assert.equal(liste.alle().length, 1);
});
it("legt eine Notiz mit der Kennung 1 an", () => {
const liste = neueListe();
liste.anlegen("Brot holen");
assert.equal(liste.alle()[0].id, 1);
assert.equal(liste.alle().length, 1);
});
}); Die erste Gruppe spart sich das Vorbereiten und legt den Zustand einmal für beide Tests an. Das funktioniert genau so lange, wie kein Test etwas verändert, und Tests verändern ständig etwas. Hier sieht der zweite eine Notiz, die der erste angelegt hat.
Das Tückische daran ist nicht der rote Test, sondern was er dir erzählt: Er sagt „die Kennung stimmt nicht”, und in Wahrheit stimmt der Zustand nicht. Solche Tests schickt man auf die falsche Fährte, und sie werden je nach Reihenfolge rot oder grün.
Die zweite Gruppe baut in jedem Test frisch auf. Das sind zwei Zeilen mehr, und dafür ist jeder Test für sich lesbar und für sich lauffähig. Das ist die Regel: Ein Test bringt alles mit, was er braucht.
Wie ein Testname aussieht
Der Name ist die Fehlermeldung, die du bekommst, wenn der Test rot wird. Er sollte also den Satz enthalten, den du dann lesen willst.
Gut: gibt bei einer leeren Liste 0 zurueck. Das beschreibt Verhalten. Wird der Test rot, weißt
du sofort, was nicht mehr stimmt, ohne den Test zu öffnen.
Schlecht: durchschnitt, test 2, funktioniert. Der erste benennt die Funktion, die anderen
gar nichts. Wenn zwölf Tests funktioniert heißen, ist die Bilanz am Ende eine Zahl und keine
Auskunft.
Eine Faustregel, die fast immer trägt: Der Name beginnt mit einem Verb und beschreibt, was
herauskommt, nicht was hineingeht. wirft bei negativem Preis ist besser als
negativer Preis, weil der Name dann für sich steht.
Warum Tests nicht auf Interna schauen
Der letzte Punkt ist der wichtigste und der am häufigsten verletzte.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
// Dieselbe Notizliste, zweimal gebaut. Von aussen verhalten sie sich gleich.
function mitArray() {
const eintraege = [];
return {
eintraege,
anlegen(text) {
const notiz = { id: eintraege.length + 1, text };
eintraege.push(notiz);
return notiz;
},
alle: () => [...eintraege],
};
}
function mitMap() {
const nach_id = new Map();
return {
nach_id,
anlegen(text) {
const notiz = { id: nach_id.size + 1, text };
nach_id.set(notiz.id, notiz);
return notiz;
},
alle: () => [...nach_id.values()],
};
}
for (const [name, baue] of [["mit Array", mitArray], ["mit Map", mitMap]]) {
describe(name, () => {
it("schaut auf die Interna", () => {
const liste = baue();
liste.anlegen("Milch kaufen");
assert.equal(liste.eintraege.length, 1);
});
it("schaut auf das Verhalten", () => {
const liste = baue();
liste.anlegen("Milch kaufen");
assert.equal(liste.alle().length, 1);
});
});
} Zweimal dieselbe Notizliste, einmal mit einem Array darin und einmal mit einer Map. Von außen
verhalten sie sich gleich: anlegen, auflisten, zählen. Der Test auf das Verhalten ist bei beiden
grün. Der Test auf die Interna bricht beim Umbau, obwohl sich am Verhalten nichts geändert hat.
Ein Test, der bei einer Umbenennung rot wird, ist eine Bremse und kein Netz. Er hindert dich daran, den Code zu verbessern, und er schützt dich vor nichts, denn ein Fehler im Verhalten wäre ihm gar nicht aufgefallen.
Das ist derselbe Gedanke, der diesen ganzen Kurs trägt und der in jeder Challenge steckt: das
Ergebnis prüfen statt den Code-Text. Keine deiner Aufgaben hat je verlangt, dass eine Variable
einen bestimmten Namen hat oder eine Schleife eine for-Schleife ist. Geprüft wurde immer, was
herauskommt, und genau so schreibst du auch deine eigenen Tests.
Praktisch heißt das: Teste über die Schnittstelle, die auch andere benutzen. Exportierte Funktionen ja, interne Hilfsfunktionen nein. Rückgabewerte ja, private Felder nein. Wenn dir dabei auffällt, dass sich etwas nur über Interna prüfen lässt, ist das oft ein Hinweis, dass der Schnittstelle etwas fehlt.
Zum Mitnehmen
Ein Test mit nur einem Normalfall beweist fast nichts. Die Fälle, die etwas beweisen, liegen an den Rändern, und der Testname beschreibt das Verhalten, nicht die Funktion.
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.