Der Rückweg zerstört die Messung: wie git checkout meine Mutationsbatterie entwertete
Eine Mutationsbatterie prüft, ob Tests wirklich anschlagen. Bei mir lief sie nach dem ersten Mutanten still gegen den alten Code weiter, weil der Rückweg die nicht committeten Korrekturen gleich mitnahm. Woran ich es gemerkt habe und was ich seitdem anders mache.
Eine Mutationsbatterie ist die ehrlichste Frage, die man einer Testsuite stellen kann: Ich baue absichtlich einen Fehler ein und sehe nach, ob ein Test rot wird. Bei mir hat diese Prüfung einmal sauber und vollständig ausgesehen, obwohl sie ab dem zweiten Mutanten gegen einen Code lief, den es so gar nicht mehr geben sollte. Schuld war nicht der Mutant, sondern der Weg zurück.
Das Muster, das jeder so baut
Der Ablauf ist naheliegend und in fünf Zeilen Shell erledigt: Zeile ändern, Tests laufen lassen, Ergebnis notieren, Datei zurücksetzen, nächster Mutant. Für das Zurücksetzen bietet sich git checkout -- datei an. Kurz, eingebaut, und es stellt garantiert einen sauberen Stand her.
for m in mutanten/*.py; do
python "$m" "$F" # Fehler einbauen
pytest -q tests/ > "log/$(basename "$m").txt"
git checkout -- "$F" # zurueck zum ... welchen Stand?
doneGenau da sitzt der Haken. git checkout -- datei stellt nicht meinen Stand her, sondern den des letzten Commits. Das fällt im Alltag nicht auf, weil man Batterien meistens nach dem Commit fährt. In meinem Fall hatte ich gerade vier Befunde aus einer Gegenlesung eingearbeitet, aber noch nicht committet. Nach Mutant 1 waren alle vier Korrekturen weg, ohne Rückfrage und ohne Meldung.
Warum das Ergebnis trotzdem plausibel aussah
Gefährlich wurde es, weil die Batterie danach nicht abbrach. Mutant 2 und 3 suchten sich ihre Stelle über eine Ankerzeile, und diese Zeilen gab es im alten Code auch. Die Mutationen wurden also angewandt, die Tests liefen, manche wurden rot, manche nicht. Die Protokolle sahen genauso aus wie gültige Ergebnisse. Sie beschrieben nur einen Code, den ich längst überarbeitet hatte.
Ein falsches Ergebnis, das aussieht wie ein richtiges, ist schlimmer als ein Absturz. Ein Absturz kostet zehn Minuten. Eine glaubwürdige Fehlmessung landet im Pull Request als „Gegenprobe bestanden“ und wird von niemandem mehr angefasst.
Das einzige Signal war ein Widerspruch
Aufgefallen ist es erst bei Mutant 4. Sein Anker kam im Code nicht mehr vor, das Skript meldete sinngemäß „Anker nicht eindeutig: 0 Treffer“. Und trotzdem fiel ein Test. Das ist ein logischer Widerspruch: Eine Mutation, die gar nicht angewandt wurde, darf nichts verändern. Wenn sich trotzdem etwas ändert, misst man nicht den Code, den man zu messen glaubt.
Ohne diesen Zufall hätte ich eine grüne Batterie gemeldet. Das ist die eigentliche Lehre: Ich habe den Fehler nicht durch Sorgfalt gefunden, sondern weil die Situation zufällig einen Widerspruch erzeugt hat. Auf Zufälle möchte ich mich ungern verlassen. Daher habe ich die Prüfung so umgebaut, dass dieser Fall nicht mehr still durchrutschen kann.
Vier Regeln, die seitdem gelten
- Erst committen, dann mutieren. Ein Zwischencommit (
git commit -am wip) kostet zehn Sekunden und macht jeden Fehlgriff pergit resetheilbar. Eine Batterie prüft einen definierten Stand. Ohne Commit prüft sie irgendeinen. - Zurück per Kopie, nicht per git. Vor dem Mutieren die Datei nach
/tmpkopieren und danach von dort zurückspielen. Noch besser: die ganze Batterie auf einer Kopie des Verzeichnisses fahren. Dann kann auch ein abgebrochener Lauf keinen Mutanten im echten Baum zurücklassen. Bei einer neuen, noch nicht versionierten Datei wäre so ein Überbleibsel ingit diffnicht einmal zu sehen. - Nach jedem Zurücksetzen eine Lebendprobe. Ein
grepauf ein Merkmal der eigenen Änderung, etwa einen neuen Funktionsnamen. Fehlt es, bricht die Batterie ab. Das ist billig und fängt genau den Fall, der mich erwischt hat. - Ein Mutant, der seinen Anker nicht findet, ist ein Fehler. Nicht „überlebt“, nicht „übersprungen“, sondern Abbruch. Ein stilles
sed, das nichts trifft, erzeugt sonst einen überlebenden Mutanten, den es nie gab.
def mutiere(pfad, alt, neu):
s = open(pfad, encoding="utf-8").read()
n = s.count(alt)
assert n == 1, f"Anker {n}x gefunden, erwartet 1: {alt[:40]!r}"
open(pfad, "w", encoding="utf-8").write(s.replace(alt, neu))Eine fünfte Frage stelle ich inzwischen bei jedem Mutanten: Welches Verhalten dreht diese Änderung eigentlich um? Ich hatte auch schon einen Mutanten, der faktisch nur einen Kommentar einfügte. Die Tests blieben grün, und das sah nach einer Lücke in der Abdeckung aus. In Wahrheit war es einfach keine Messung.
Fazit
Bei einer Gegenprobe schaut man fast nur auf den Hinweg, also auf die Frage, welchen Fehler man einbaut. Der Rückweg wirkt wie Verwaltung und bekommt keine Aufmerksamkeit. Dabei entscheidet er, ob alle folgenden Messungen noch etwas bedeuten. Wer mit git checkout zurücksetzt, setzt auf den Commit zurück und nicht auf die eigene Arbeit. Seitdem committe ich vor der Batterie, setze per Kopie zurück und lasse jeden Mutanten abbrechen, der seinen Anker nicht findet. Das klingt nach Pedanterie. Es ist aber billiger, als eine gründliche Prüfung zu melden, die gegen den falschen Stand lief.
- #mutationstests
- #git
- #testing
- #python
- #qualitaetssicherung
- #code-review
Weiterlesen
Ein Test-Double, das nicht falsch antworten kann, prüft nichts
27. Oktober 2026 · Engineering, Testing, KI-Agenten · 4 Min. Lesezeit
Zwei grüne Pull Requests, zusammen rot: was Zweig-CI nicht messen kann
17. Oktober 2026 · Engineering, KI-Agenten, Automatisierung · 5 Min. Lesezeit
Zeichen sind keine Token: wie ein Embedding-Stapel an 300.000 Token scheiterte
13. November 2026 · RAG, KI-Agenten, Engineering · 5 Min. Lesezeit