Flaky Tests sind ein Architekturproblem

Massives geschlossenes Schleusentor in einer dunklen Halle, davor stauen sich leuchtend cyanfarbene Datenpakete; in der Statusleuchten-Reihe des Tors flackert eine einzige Lampe grell amber
Abbildung 1. Ein flackernder Test genügt: Das Gate bleibt zu, und die Arbeit staut sich davor

Jedes Team kennt sie: Tests, die fast immer grün sind. Man startet den Lauf neu, diesmal geht es durch, und weiter im Text. Diese Duldung hat einen stillen Preis, aber er ist klein genug, dass man ihn jahrelang zahlt.

Bei mir wurde der Preis schlagartig sichtbar, als kein Mensch mehr auf den Wiederholen-Knopf drücken konnte. In meinem Nebenprojekt Calculoria arbeitete ein autonomer Loop den Backlog ab, mit einer harten Regel: Gemergt wird nur, wenn Formatprüfung, Build und die komplette Testsuite grün sind (das Protokoll dieses Experiments habe ich separat aufgeschrieben). Für einen solchen Loop ist ein Test, der in einem von zehn Läufen rot wird, kein Ärgernis. Er ist ein geschlossenes Tor.

Der Fall

Sechs UI-Tests fielen im Gate aus, aber nicht immer und nie dieselben zuerst. Einzeln oder in kleiner Runde ausgeführt: zuverlässig grün. Unter der vollen Last des Loops, wenn mehrere Agenten parallel bauten und testeten: sporadisch rot. Das Muster war über sechs Gate-Durchläufe und fünf voneinander unabhängige Agenten reproduzierbar, es war also kein Zufall und keine Einbildung.

Die Ursache war so banal wie lehrreich: Die Tests enthielten echte Wartezeiten. Komponenten, die nach einer Verzögerung etwas anzeigen, wurden getestet, indem der Test real wartete. Auf einer entspannten Maschine reicht die Frist immer. Auf einer Maschine, die gerade vier andere Testsuiten, zwei Builds und einen Formatlauf stemmt, reicht sie manchmal nicht. Der Test prüfte nicht das Verhalten der Komponente, sondern die Auslastung des Rechners.

Warum das ein Architekturproblem ist

Es ist verlockend, so etwas als Test-Hygiene abzutun: Frist verlängern, Test stabilisieren, fertig. Aber die längere Frist macht die Suite nur langsamer und die Wette auf die Maschinenlast etwas günstiger. Das Problem liegt eine Ebene tiefer: Der Produktionscode behandelte Zeit nicht als Abhängigkeit, sondern als Selbstverständlichkeit. Wo Zeit hart verdrahtet ist, kann der Test sie nicht kontrollieren, also muss er real warten, also hängt sein Ergebnis an Dingen, die mit dem geprüften Verhalten nichts zu tun haben.

Ein flackernder Test zeigt fast immer auf eine solche versteckte Kopplung: an die Uhr, an den Scheduler, an die Reihenfolge von Nebenläufigkeit, an geteilten Zustand zwischen Tests, an das Netzwerk. Flakiness ist selten ein Test-Problem. Sie ist die Testsuite, die auf eine Design-Entscheidung zeigt, die man nie bewusst getroffen hat.

Die Lösung in zwei Stufen

Im ersten Durchlauf war keine Zeit für Grundsatzarbeit, und das ist in Ordnung: Die sechs Tests wurden markiert und übersprungen, dazu ein Schuldenticket mit klarem Auftrag. Das ist Triage, und Triage ist legitim, solange sie ein Ablaufdatum hat. Ein dauerhaft übersprungener Test ist gelöschte Abdeckung, die noch im Repository wohnt.

Die eigentliche Lösung kam im zweiten Durchlauf und war klassische Dependency Inversion: eine schmale Abstraktion für Verzögerungen, analog zu der Uhr-Abstraktion, die das Projekt für Zeitstempel längst hatte. Der Produktionscode fordert eine Wartezeit an, statt sie selbst auszuführen. Im Betrieb wartet die echte Implementierung; im Test läuft eine kontrollierte, die sofort weiterschaltet. Aus „der Test wartet und hofft" wurde „der Test spult die Zeit vor und weiß". Danach lief die volle Suite mit über 2.800 UI-Tests dreimal in Folge fehlerfrei durch, unter derselben Volllast, unter der sie vorher fiel.

Diagramm der Delay-Abstraktion: Der Produktionscode hängt an einem Interface für Verzögerungen; im Betrieb wartet eine echte Implementierung, im Test spult eine kontrollierte Implementierung die Zeit sofort vor
Abbildung 2. Dependency Inversion für Zeit: Die Komponente fordert Wartezeit an, statt selbst zu warten

Der Aufwand für die Abstraktion: überschaubar, ein Interface und eine Handvoll Anpassungen. Die Wirkung: Eine ganze Fehlerklasse ist strukturell verschwunden, nicht statistisch seltener geworden.

Der Maßstab verschiebt sich

Solange Menschen die letzte Instanz vor dem Merge sind, kann man sich wackelige Tests leisten, weil Menschen Kontext haben und Geduld. In dem Moment, in dem ein automatisiertes Gate entscheidet, ob Arbeit integriert wird, ändert sich die Rechnung: Jeder flackernde Test blockiert dann echte Arbeit, erzeugt falsche Fehlersuchen und untergräbt das Vertrauen in genau das Signal, auf dem die ganze Automatisierung ruht.

Deshalb halte ich es inzwischen so: Ein Test, der zweimal ohne Codeänderung unterschiedlich ausgeht, ist ein Vorfall mit Ticket, nicht eine Anekdote im Teamchat. Die sechs übersprungenen Tests aus dem ersten Lauf hatten genau so ein Ticket, mit Auftrag und Frist. Sie sind heute wieder aktiv, alle sechs.


Klingt nach deinem Thema? Schreib mir. Im Erstgespräch klären wir unverbindlich, ob und wie ich helfen kann.