Zum Inhalt springen
← Zurück zum Blog
Team Stories

Ein Tag, zehn Fehler

Camsol · · 5 Min. Lesezeit

Halb zehn am Morgen, der Rechner lief seit einer Stunde. Wir hatten uns einen Tag gegeben, um einen kleinen internen Dienst zu bauen: Er sollte ein Verzeichnis beobachten, eingehende Dateien verarbeiten, jeden Durchlauf protokollieren und Alarm schlagen, wenn etwas hängen bleibt. Nichts daran war neu, nichts daran war schwer. Am Abend lief er, und wir hatten zehn Fehler gefunden.

Keinen davon hatte die Testsuite gefunden. Die war grün, den ganzen Tag über, auch in den Stunden, in denen der Dienst nachweislich das Falsche tat.

Das ist keine Anklage gegen Tests. Die Suite tat genau das, wofür wir sie geschrieben hatten, und sie hat uns an mehreren Stellen davor bewahrt, beim Umbauen etwas kaputtzumachen. Sie konnte diese zehn Fehler nur nicht sehen. Jeder einzelne kam vom Laufenlassen - davon, dass jemand den Dienst gestartet, ihm eine echte Datei hingelegt und danach nachgesehen hat, was tatsächlich passiert ist.

Drei davon lohnen sich zu erzählen, weniger wegen der Fehler selbst als wegen dem, was sie miteinander teilen.

Eine Berechtigung, die lokal nie auffällt

Der Dienst schrieb seine Ergebnisse in ein Verzeichnis, das er nicht selbst angelegt hatte. Auf dem eigenen Rechner ist das unsichtbar: Dort läuft alles unter deinem Benutzer, der Ordner gehört dir, und die Frage, ob geschrieben werden darf, stellt sich nie. In der Testumgebung lief der Dienst unter einem eigenen Nutzer, und das Verzeichnis kam aus einem Setup-Skript, das jemand anderem gehörte.

Der Test hatte diese Frage nie gestellt, und zwar aus einem sehr nachvollziehbaren Grund: Er legte sein Verzeichnis selbst an, in einem temporären Pfad, direkt bevor er den Dienst aufrief. Ein Verzeichnis, das der Testprozess gerade erzeugt hat, gehört dem Testprozess. Es kann gar nicht anders, als schreibbar zu sein.

Die Lehre steckt nicht darin, dass Berechtigungen kompliziert sind. Sie steckt darin, dass ein Test, der sich seine Umgebung selbst baut, immer eine bequeme Umgebung baut. Genau das macht ihn schnell und wiederholbar - und genau das lässt ihn diese Klasse von Fehlern nicht sehen.

Ein Prüfer, der dasselbe Verzeichnis benutzt wie der Autor

Der zweite Fehler war der unangenehmste, weil er wie eine Bestätigung aussah. Nach jedem Durchlauf prüfte eine kleine Routine, ob die Ausgabedatei da war, wo sie hingehörte. Sie meldete zuverlässig Erfolg.

Sie meldete Erfolg, weil sie den Pfad über dieselbe Hilfsfunktion auflöste wie der schreibende Teil. Beide fragten dieselbe Stelle, wo die Datei liegen soll, und beide bekamen dieselbe Antwort. Als diese Antwort falsch wurde - eine Konfiguration griff nicht, der Pfad landete eine Ebene zu hoch -, schrieb der Autor am falschen Ort, und der Prüfer sah genau dort nach. Er bestätigte, was der Autor getan hatte. Beide lagen in dieselbe Richtung daneben.

Ein Prüfer, der seine Annahmen vom Geprüften erbt, prüft nichts. Er wiederholt nur. Das ist im Kleinen dieselbe Frage, die auch bei Reviews wichtig ist: Wer nachsieht, muss aus einer anderen Richtung kommen als der, der gebaut hat, sonst sieht er dieselben Dinge und übersieht dieselben.

Ein Protokoll, das nicht schreibbar ist, während der Alarm gesund aussieht

Der dritte Fehler war der stillste. Das Protokoll landete in einer Datei, die aus einer früheren Version des Setups stammte und dem Dienst nicht mehr gehörte. Der Logger fing den Fehler ab, wie Logger das tun - ein Dienst soll nicht sterben, nur weil er seine Zeile nicht loswird -, und schrieb ins Leere.

Der Alarm blieb grün. Er war so gebaut, dass er den Zustand des Prozesses meldete: läuft, antwortet, verbraucht nichts Auffälliges. Das stimmte auch alles. Nur hatte niemand ihn danach gefragt, ob in der letzten Stunde eine Zeile geschrieben worden war. Zwei stille Tage hätten von außen genauso ausgesehen wie zwei gesunde.

Ein Alarm, der meldet, dass ein Prozess lebt, meldet nicht, dass er arbeitet. Das klingt banal, wenn man es aufschreibt. Es fällt trotzdem niemandem auf, solange der Prozess tatsächlich arbeitet.

Das Muster

Der interessante Teil ist nicht die Zehn. Zehn Fehler an einem Tag sind für ein frisch gebautes System keine bemerkenswerte Zahl, weder in die eine noch in die andere Richtung. Interessant ist, wo sie alle saßen.

Keiner der zehn steckte in einer Funktion. Sie steckten in der Naht zwischen dem Programm und allem drumherum: dem Dateisystem, den Benutzern, den Berechtigungen, der Uhr, dem Prozess nebenan. Ein Test ersetzt dieses Drumherum durch eine handliche Version davon. Das ist kein Mangel, das ist der Zweck der Übung - deshalb läuft die Suite in Sekunden und deshalb sagt sie bei jedem Umbau verlässlich, ob die Logik noch stimmt. Es ist nur eben auch der Grund, warum sie an dieser Naht blind ist.

Die Konsequenz ist nicht, mehr Tests zu schreiben. Sie ist auch nicht, für alles Integrationstests zu bauen, denn die sind teuer und bauen sich am Ende ihre eigene handliche Umgebung.

Was wir mitgenommen haben

Drei Dinge, alle unspektakulär.

Lass es früh laufen, und zwar irgendwo, das nicht dein Rechner ist. Der halbe Tag, an dem etwas nur lokal existiert, ist der Tag, an dem sich alle Annahmen über die Umgebung unbemerkt einnisten.

Lass jemand anderen nachsehen als den, der gebaut hat. Nicht als Ritual, sondern weil der Autor unweigerlich dort nachschaut, wo er die Sache hingelegt zu haben glaubt.

Und frag nach dem Beleg, nicht nach dem Status. “Läuft es?” bekommt eine Antwort, die auf einer Vermutung beruht. “Zeig mir die letzte Zeile im Protokoll” bekommt eine Antwort, die auf etwas beruht, das tatsächlich passiert ist. Der Unterschied zwischen beiden Fragen war an diesem Tag ungefähr die Hälfte der Ausbeute.

Am nächsten Morgen war die Testsuite immer noch grün, und das war völlig in Ordnung. Die zehn Fehler steckten nie in dem, was sie abdeckte. Sie steckten in dem, was wir für so selbstverständlich hielten, dass wir es gar nicht erst aufgeschrieben hatten.

Haben Sie Projektbedarf?

Tobias

Gespräch vereinbaren →