Sechs Monate habe ich Tests geschrieben, die nie einen Fehler gefunden haben. Sie waren grün, sie waren schnell, sie haben meinen Code bestätigt. Bestätigung ist aber keine Prüfung. Das ist ein Spiegel mit Häkchen.
Der erste Test, der wirklich etwas gefunden hat, war zwölf Zeilen lang, hatte einen unangenehmen Namen und hat einen Fehler aufgedeckt, den ich selbst gebaut hatte. Er hing an einer Zeitzone. Natürlich hing er an einer Zeitzone.
Tests, die nur sich selbst prüfen
Mein erster Test hat geprüft, dass die Summenfunktion zwei Zahlen addiert. Der zweite hat einen Mock aufgerufen und geprüft, dass der Mock aufgerufen wurde. Der dritte hat die Ausgabe einer Funktion mit dem verglichen, was die Funktion gerade ausgegeben hatte — ein Snapshot, den ich mit dem Befehl erzeugt hatte, der ihn prüft.
// so sieht ein Test aus, der nichts weiss
it("ruft das Repository auf", async () => {
const repo = { find: jest.fn().mockResolvedValue([]) };
await listOrders(repo);
expect(repo.find).toHaveBeenCalled();
// und wenn listOrders danach alles wegwirft: immer noch gruen
});
Das Muster dahinter ist einfach: Diese Tests stellen Fragen über mich, nicht über das Programm. „Habe ich den Mock aufgerufen" ist eine Frage über meinen Test. „Zeigt die Liste den richtigen Tag" ist eine Frage über das Produkt. Der erste Typ ist bequem, weil er grün bleibt, solange ich nichts anfasse. Der zweite ist unbequem, weil er rot wird, wenn ich etwas falsch gemacht habe.
Ein Test, der alles mockt, prüft am Ende, ob ich den Mock richtig getippt habe. Das ist kein Unit-Test, das ist ein Tipptest. Nützlich ist er trotzdem: Er zeigt mir, dass die Verdrahtung steht. Nur nicht, dass die Rechnung stimmt.
Der Test, der den Fehler gefunden hat
Es ging um eine Liste von Buchungen mit Datum. Die Anzeige zeigte den Tag, und mein Code nahm den ersten Teil des ISO-Strings. Das funktioniert für alle, die tagsüber buchen, und für alle, die in UTC leben. Für alle anderen ist es eine kleine Lüge mit Datum.
it("zeigt den Tageswechsel in Zuercher Zeit, nicht in UTC", () => {
// 23:30 UTC am 30. Juni ist 01:30 am 1. Juli in Zuerich
const d = new Date("2024-06-30T23:30:00Z");
expect(formatDay(d)).toBe("01.07.");
});
Der Test war rot. Meine Funktion war `d.toISOString().slice(5, 10)` und gab „06-30" zurück, also den 30. Juni. Für eine Buchung um halb eins nachts war das der falsche Tag, in der Anzeige, in der Liste, in dem Moment, in dem jemand seine Ausgaben kontrolliert. Der Fix war eine Formatierung mit expliziter Zeitzone, vier Zeilen, und ich habe länger nach dem richtigen Testnamen gesucht als nach dem Fix.
Warum dieser Test funktioniert hat
| Test | Was er prüft | Findet er Fehler? |
|---|---|---|
| Summe von zwei Zahlen | Die Sprache, nicht meinen Code | Nein |
| Snapshot der Komponente | Gleichheit mit dem letzten Stand | Manchmal, aber du weißt nicht warum |
| Datum am Tagesrand | Formatierung am Rand | Ja, genau meinen |
| Rundung von 0,1 plus 0,2 | Gleitkomma der Plattform | Ja, aber nicht in meinem Code |
Drei Dinge waren anders als bei meinen früheren Tests. Erstens: Der Test beschreibt eine Anforderung, nicht eine Implementierung. Er sagt „der Tag für Nutzer in Zürich", nicht „diese Funktion ruft jene Funktion". Zweitens: Er testet einen Rand, nicht die Mitte. Die Mitte funktioniert immer, sonst hättest du sie beim Klicken bemerkt. Drittens: Er hätte bei jedem Refactoring überlebt, das die Anzeige nicht ändert.
Und der Test war billig. Zwölf Zeilen, keine Fixture, keine Datenbank, ein Datum und ein String. Sechs Monate lang hatte ich komplizierte Tests geschrieben, weil ich dachte, ein guter Test braucht Aufbau. Braucht er nicht. Er braucht einen Fall, den du sonst nie ausprobierst.
Was ich heute teste und was nicht
Ränder sind billig zu testen und teuer zu übersehen. Die Mitte testet der Alltag für mich, jeden Tag, ungefragt, meistens um 14:07.
Ich teste Ränder: leere Liste, ein Element, sehr viele Elemente, Datum um Mitternacht, Betrag 0, doppelte IDs. Ich teste nicht mehr, dass eine private Hilfsfunktion genau einmal aufgerufen wird. Das ist eine Aussage über meine aktuelle Struktur, und die ändert sich nächste Woche, ohne dass jemand etwas falsch macht.
Was ich auch nicht mehr mache: Tests als Sicherheitsnetz für Umbauten verkaufen, die niemand geplant hat. Wenn ich eine Funktion umbaue und danach sieben Tests anpasse, habe ich nicht getestet, ich habe Buch geführt. Ein Test, der bei jedem Umbau rot wird, erzieht dich zu genau einer Fähigkeit: ihn anzupassen.
Und ich schreibe den Testnamen so, dass er als Fehlermeldung funktioniert. „zeigt den Tageswechsel in Zuercher Zeit" sagt mir beim roten Lauf, was kaputt ist. „formatDay works" sagt mir, dass ich vor sechs Monaten keine Lust hatte.
Fazit
Sechs Monate grüne Tests, ein roter, der zählt. Der Preis, Tests als Bestätigung zu schreiben: Du hast eine Zahl im Dashboard, die steigt, und keine Aussage darüber, ob dein Code stimmt. Der Preis, Tests als Frage zu schreiben: ein paar Minuten mehr pro Test und die unangenehme Erkenntnis, dass dein Code den Randfall nicht kann.
Ich nehme den zweiten Preis. Er wird jedes Mal kleiner, und der erste wächst.
Hast du auch einen Test, der erst um 01:30 rot wird, und wie lange hat es gedauert, bis du ihm geglaubt hast?
danielroth
01:30 ist die stunde wo zeitzonen und sommerzeit zuschlagen. bei uns läuft alles in utc und die anzeige ist problem der schicht darüber, also testet das keiner 🤷
mariehoffmann
ein test mit unangenehmem namen beschreibt einen fall. 'should work' prüft nichts, 'bucht am 31.03. um 01:30 in europe/berlin' prüft alles
leoniefischer
sechs monate grüne tests die nur bestätigen was der code tut. ich hab dieselbe phase gehabt und erst mit einem produktionsfehler verstanden was fehlt
annakeller
zeitzonen brauchen ein festes datum und eine feste zone im test. wer new date() benutzt testet die uhr des ci-runners, z.b. 03:00 statt 01:30