DoMe Dynamics
AI Agent Systems
Zurueck zum Log
testingpytesttest-isolationagentic

Flaky Test durch Modul-Reload: importlib.reload leert eine Registry im Prozess-Speicher

Grün allein, rot in der Suite — ein Leck, das in keiner Datei stand

Ein Test bestand isoliert, fiel aber in der vollen Suite durch. Die Ursache lebte nicht im Dateisystem, sondern im Prozess-Speicher: ein anderer Test hatte ein Modul neu geladen und dabei eine Registry leergeräumt.

Der Test war grün. Allein ausgeführt, jedes Mal, zuverlässig. In der vollen Suite war er rot. Auch das zuverlässig. Genau diese Kombination — isoliert grün, gemeinsam rot — ist eines der irritierendsten Signale, die eine Testsuite geben kann, weil sie der Grundannahme widerspricht, dass ein Test eine in sich abgeschlossene Aussage ist.

Der konkrete Fall stammt aus der Dual-Bridge, der Schicht, über die zwei KI-Modelle abwechselnd Code bauen und reviewen. Ein Test prüfte, dass ein Auftrag korrekt an den Codex-Runner geroutet wird und am Ende den Status done trägt. Allein lieferte er done. In der Suite lieferte er error.

Das erste Misstrauen gilt der Reihenfolge

Wenn ein Test nur im Verbund scheitert, ist die erste Frage nicht „was ist an diesem Test falsch", sondern „was läuft vor ihm". Denn ein Test, der allein besteht, ist für sich korrekt. Was ihn umbringt, muss von außen kommen — von etwas, das ein anderer Test hinterlassen hat.

pytest führt Tests in einer bestimmten Reihenfolge aus, und diese Reihenfolge ist stabil genug, dass „läuft Datei A vor Datei B" reproduzierbar ist. Ein paar gezielte Läufe — erst der verdächtige Test allein, dann mit jeweils einer anderen Datei davor — engen den Schuldigen ein. Hier zeigte sich: Lief eine bestimmte andere Testdatei vorher, war der Test rot. Lief sie nicht, war er grün. Damit war klar, dass es kein Bug im Test selbst war, sondern ein hinterlassener Zustand.

Das Leck: ein neu geladenes Modul räumt die Registry leer

Die schuldige Testdatei tat etwas, das harmlos aussieht: importlib.reload(runners). Sie lud das Runner-Modul neu, um es in einem definierten Ausgangszustand zu testen. Ein reload führt den Modul-Code aber von vorn aus — und dabei wurde die globale Runner-Registry auf ihren Initialwert zurückgesetzt: nur der Test-echo-Runner, sonst nichts. Die echten Adapter, die beim ersten Import registriert worden waren, waren weg.

Der spätere Test rechnete mit RUNNERS["codex"]. Nach dem reload existierte dieser Eintrag nicht mehr. Statt den Codex-Runner zu finden, lief der Auftrag in einen Fehlerpfad — daher error statt done. Dazu kam eine zweite Quelle desselben Problems: Umgebungsvariablen, die ein Test setzte, ohne sie danach aufzuräumen. DUAL_BRIDGE_*-Werte leben im Prozess-Speicher und überdauern die einzelne Testfunktion, wenn niemand sie zurücksetzt.

Beide Lecks haben dieselbe Eigenschaft: Sie hinterlassen keinen Abdruck im Dateisystem. Man kann das gesamte Repo durchsuchen und findet nichts, weil der veränderte Zustand in flüchtigem Prozess-Speicher liegt — in einem Modul-Global, in os.environ. Diese Art von Leck ist unsichtbar, bis man die Ausführungsreihenfolge als Verdächtigen ernst nimmt.

Der Fix: pro Test ein sauberer Tisch

Die Behebung war eine autouse-Fixture in der conftest.py, die vor jedem Test einen Schnappschuss des relevanten Zustands macht und ihn danach wiederherstellt: die komplette DUAL_BRIDGE_*-Umgebung und die Runner-Registry. Jeder Test bekommt damit denselben Ausgangszustand, egal was der vorherige getan hat.

Das ist der eigentliche Punkt: Test-Isolation heißt nicht nur, eine eigene Datenbank oder ein eigenes Temp-Verzeichnis zu haben. Es heißt auch, jeden geteilten, veränderlichen Zustand zu kapseln — und dazu gehören Modul-Globals und Umgebungsvariablen, die man leicht übersieht, weil sie nicht nach „Zustand" aussehen. monkeypatch.setenv statt direktem os.environ-Schreiben und autouse-Fixtures für Registries sind das Minimum, nicht die Kür.

Warum „allein grün" eine gefährliche Beruhigung ist

Es liegt nahe, einen Test zu schreiben, ihn isoliert grün zu sehen und ihn damit für fertig zu halten. Aber die isolierte Ausführung ist nicht der reale Fall. Im echten Lauf — in CI, vor einem Merge — läuft die volle Suite, und dort entscheidet sich, ob der Test trägt. Ein Test, der nur allein besteht, gibt eine falsche Sicherheit: Er behauptet eine Eigenschaft, die unter realen Bedingungen nicht gilt.

Schlimmer noch: Solche Lecks sind nicht-deterministisch in dem Sinn, dass sie an der Reihenfolge hängen. Eine neue Testdatei mit einem alphabetisch früheren Namen kann ein Leck einschleusen, das vorher nie sichtbar war — und plötzlich ist ein ganz anderer Test rot, ohne dass jemand ihn angefasst hat. Das ist die Sorte Fehler, die Stunden frisst, weil der offensichtliche Verdächtige (der rote Test) der Falsche ist.

Was offen ist

Die autouse-Fixture kapselt den bekannten Zustand — die DUAL_BRIDGE_*-Env und die Runner-Registry. Aber sie ist eine Aufzählung: Sie schützt genau das, woran ich gedacht habe. Das nächste Modul-Global, das jemand einführt und nicht in den Snapshot aufnimmt, kann dasselbe Problem neu erzeugen. Eine robustere Lösung wäre, geteilten veränderlichen Zustand strukturell zu vermeiden — Registries als Fixtures injizieren statt als Modul-Globals zu halten, sodass es gar keinen prozessweiten Zustand gibt, der lecken könnte. Bis dahin gilt die Disziplin: Wer einen Modul-Global oder eine Umgebungsvariable einführt, muss im selben Atemzug klären, wie sie pro Test zurückgesetzt wird.


Tests, die nur in der vollen Suite rot werden, führen fast immer zuerst zum falschen Verdächtigen. Wenn du so einen Fall hattest, schreib mir.