Fallobst im Gras unter einem Apfelbaum, aufgenommen im warmen Licht der tiefstehenden Sonne. Am oberen Bildrand sind der Stamm und sein Schatten zu sehen, davor liegen dicht an dicht rote Äpfel, von denen viele Druckstellen haben. Die Bildsprache greift die Metapher auf, mit der Marco Pivetta Mutation Testing beschreibt: Wer den Baum schüttelt, erfährt nicht, wie viel er trägt, sondern welche Früchte nicht mehr gehalten haben.
Wer den Baum schüttelt, erfährt, was hält.

Neulich ist mir eine Videodatei in die Hände gefallen, mit der ich zunächst nichts anfangen konnte. Dann fiel es mir wieder ein: zwei Stunden Gespräch mit Marco Pivetta und Stefan Priebsch, aufgezeichnet im April 2022. Ich wollte damals etwas aus dem machen, was ich daraus mitgenommen hatte. Geworden ist daraus nichts, bis jetzt.

Ich habe mir die Aufzeichnung noch einmal angesehen. Die Werkzeuge haben sich an einigen Stellen weiterentwickelt, und darauf gehe ich unten ein. Die Fragen, um die wir damals gekreist sind, sind dieselben geblieben.

Eine Frage aus der Praxis

Der Anstoß war eine ganz praktische Frage aus meiner täglichen Arbeit. Wo hört die Verantwortung der Tests auf, und wo fängt die des Typsystems an?

Stefan experimentierte zu dieser Zeit mit Path Coverage in PHPUnit und stieß dabei auf Schwierigkeiten. Die Darstellung war schwer zu lesen. Selbst ein Blick auf den Bytecode mit VLD brachte keine Klarheit. Bei 100 % Line Coverage und 100 % Branch Coverage blieben in der Path Coverage Fragezeichen stehen. Das ist ein deutlicher Hinweis darauf, dass die gewohnten Metriken nicht das ganze Bild zeigen.

An dieser Stelle habe ich eingeworfen, dass hier eigentlich Mutation Testing ansetzen müsste. Marco hatte sich damals intensiv damit beschäftigt und vertrat eine Position, mit der ich nicht gerechnet hatte: Code Coverage sei ihm völlig gleichgültig. Das war der Moment, in dem ich das Thema mit den beiden gründlich durchsprechen wollte. Was Path Coverage und Mutation Testing jeweils messen, habe ich später in einem eigenen Artikel gegenübergestellt.

Erst Typen, dann Coverage, dann Mutationen

Marco arbeitet in einer Nische, in der er sich sichtlich wohlfühlt: Mutation Testing zusammen mit statischer Analyse. Für ihn sind das keine getrennten Werkzeuge; sie verstärken sich gegenseitig. PHP nennt er eine „squishy“ Sprache, dynamisch und mit wenig eingebauter Absicherung. Für ihn ist das der Grund, es mit den Werkzeugen zu übertreiben.

Sein Vorgehen hat drei Stufen. Am Anfang steht maximale Typisierung. Auf den Einwand, wer Typen habe, brauche keine Tests, reagierte er unmissverständlich: Unsinn. Typen ersetzen keine Tests. Sie machen den Code aber starr, und darauf kommt es ihm an. Eine Funktion mit vollständigen Typen tut eine Sache und läuft nicht mit irgendwelchen Variablen nach links und rechts davon.

Danach kommt Code Coverage als Basismetrik. Und schon hier stellt sich die eigentliche Frage: Ist diese Abdeckung inhaltlich sinnvoll oder nur ein Nebenprodukt? Diese Frage beantwortet die dritte Stufe. Mutation Testing prüft nicht, ob Code ausgeführt wurde, sondern ob die Tests tatsächlich prüfen, was sie prüfen sollen.

Der Baum und die Früchte

Marco hat für Mutation Testing ein Bild benutzt, das mir seitdem nicht mehr aus dem Kopf geht. Es sei wie ein Tree Shake. Man gehe zu einem Baum und rüttele daran, bis etwas herunterfällt. Dann wisse man, dass diese Früchte vielleicht schon etwas zu reif waren.

Das Bild trägt in beide Richtungen, und die zweite Richtung wird meiner Erfahrung nach oft übersehen. Ein überlebender Mutant kann bedeuten, dass ein Test zu oberflächlich ist. Er kann aber genauso bedeuten, dass an dieser Stelle Code steht, der nichts tut. Dead Code also, oder ein Zweig, den eine präzisere Typisierung längst unerreichbar gemacht hat.

Ein einfaches Beispiel dafür:

<?php declare(strict_types=1);
final class ShoppingCart
{
    /**
     * @var list<Item>
     */
    private array $items = [];

    public function numberOfItems(): int
    {
        return (int) count($this->items);
    }
}

Die Tests für diese Klasse können vollständige Abdeckung erreichen. Entfernt Infection nun den (int)-Cast, bleiben sie trotzdem grün. Das ist keine Lücke in den Tests, denn count() gibt bereits einen int zurück. Der Cast hat nie etwas bewirkt. Ein Test, der diesen Mutanten tötet, lässt sich gar nicht schreiben. Die richtige Reaktion ist, den Cast zu löschen und die Zusicherung dem Rückgabetyp zu überlassen.

Das ist der Punkt, an dem sich die Ausgangsfrage von selbst beantwortet. Wo der Rückgabetyp die Zusage macht, braucht es weder einen Cast noch einen Test dafür.

Für eine verwandte Sorte von Früchten muss der Baum seit PHPStan 2.3 gar nicht mehr geschüttelt werden. PHPStan meldet jetzt Werte, die zugewiesen, aber nie gelesen werden: eine Zuweisung, die vor dem ersten Lesen überschrieben wird, einen Zähler, der hochgezählt, aber nirgends verwendet wird, einen Parameter, den eine private Methode ignoriert. Genau an solchen Stellen überleben Mutanten. Wenn Infection aus $imported + 1 ein $imported - 1 macht, ändert es einen Wert, den niemand liest, und kein Test kann das bemerken. Bisher hat erst der Bericht von Infection auf diesen Code gezeigt, jetzt tut es die statische Analyse, bevor überhaupt ein Mutant erzeugt wird. Die Reaktion ist dieselbe wie beim Cast: löschen. Die neuen Regeln sind vorerst nur mit Bleeding Edge aktiv.

Sinnlose Mutationen

Den kritischen Einwand dazu hat Stefan geliefert. Er hatte beobachtet, dass viele Mutationen schlicht sinnlos sind. Sein Beispiel war ein Raytracer, ein durch und durch mathematisches Projekt mit Matrizen, Vektoren und Strahlen. Viele der dort verwendeten Operationen sind kommutativ, es gilt also A + B = B + A. Mutation Testing vertauscht die Operanden trotzdem und erwartet, dass ein Test das bemerkt. Bemerken kann das kein Test, denn das Ergebnis bleibt dasselbe.

Solche Mutanten kosten Rechenzeit und drücken den Mutation Score, ohne dass jemand etwas daraus lernt. Die Frage, die sich daraus ergibt, lautet: Wie wird Mutation Testing klüger?

Marcos Antwort darauf war das Roave Infection Static Analysis Plugin, das er zu diesem Zeitpunkt bereits entwickelt hatte. Es prüft mit Psalm, ob ein Mutant nach den Regeln des Typsystems überhaupt möglich ist. Wer eine Mutation erzeugt, die das Typsystem ohnehin ausschließt, muss sie nicht auch noch ausführen. Das Plugin gibt es weiterhin, und es funktioniert. Sein README nennt den eigenen Ansatz allerdings selbst reichlich hemdsärmelig, weil Infection damals keine Schnittstelle für Erweiterungen dieser Art hatte.

Hier hat sich seit dem Gespräch am meisten getan. Infection bringt seit Version 0.30 eine eigene Anbindung an statische Analyse mit. Mit --static-analysis-tool=phpstan läuft PHPStan über die Mutanten, die den Tests entkommen sind, und tötet diejenigen, die gegen die deklarierten Typen verstoßen. In den Berichten wird ausgewiesen, welche Mutanten von der Testsuite und welche von der statischen Analyse getötet wurden. Ein Plugin, das sich in Infection hineinhängt, braucht es dafür nicht mehr.

Auch auf der anderen Seite ist Arbeit hineingeflossen. PHPStan 2.1.34 hat im Januar 2026 den Aufwand beim Start deutlich gesenkt, und zwar ausdrücklich mit Blick auf den Einsatz während des Mutation Testing. Wenn ein Werkzeug pro Mutant einmal hochfahren muss, zählt jede Sekunde. Das war erst der Anfang. Laut der Ankündigung von PHPStan 2.3 braucht eine Analyse des Quelltexts von WordPress ohne Result Cache inzwischen knapp 8 Sekunden, im Dezember 2025 waren es noch 60. Ohne die Erweiterung PHPStan Turbo, die Teile von PHPStan in C++ neu implementiert, sind es knapp 20 Sekunden. Die Erweiterung liegt dem Composer-Paket bei und wird bei den meisten Installationen automatisch aktiviert.

Was Mutation Testing nicht leistet

Marco hat von einem Kunden berichtet, der mit CodeIgniter 2 arbeitete. Für neuen Code führte das Team ein striktes Regime ein: ein Mutation Score von mindestens 60 %, vollständige Typisierung und Ausnahmen von den Regeln nur nach Diskussion im Team. Nach einem Jahr schrieben sie nach Marcos Urteil ausgesprochen sauber und gut getestet. Die Diskussionen in den Pull Requests hatten sich von handwerklichen Fragen auf architektonische verschoben.

Bemerkenswert fand ich einen Nebensatz. Dasselbe Team hatte wöchentlich Rückfragen zu einzelnen Mutationsergebnissen. Mutation Testing läuft eben nicht von allein. Es braucht jemanden, der die Ergebnisse einordnen kann und den Überblick über die Architektur hat.

Für regulatorische Anforderungen taugt Mutation Testing ohnehin nicht. Path Coverage ist mathematisch definiert und lässt sich nachprüfen, weshalb sie in sicherheitskritischen Bereichen vorgeschrieben wird. Der Mutation Score dagegen ist ein bewegliches Ziel. Mutationen lassen sich jederzeit neu erfinden, und mit jeder neuen Version eines Werkzeugs verändert sich die Bezugsgröße. Ein Mutation Score von 100 % heißt nur, dass ein bestimmtes Werkzeug in einer bestimmten Version keine überlebenden Mutanten mehr erzeugt.

Manipulierbar ist die Zahl auch, nur mit mehr Aufwand als bei der Code Coverage. Ich habe Marco von einem Entwickler erzählt, der wegen frisierter Coverage-Zahlen entlassen wurde, weil an ihnen ein Bonus hing. Marcos Einschätzung dazu fand ich treffend: Wer klug genug ist, den Mutation Score zu manipulieren, hat meistens auch verstanden, wozu er da ist, und lässt es dann sein. Was passiert, wenn eine Metrik zur Vorgabe wird, habe ich an anderer Stelle ausführlicher beschrieben.

Kleine Komponenten

Gegen Ende hat Stefan eine These formuliert, über die ich danach länger nachgedacht habe. Hohe Werte im Mutation Testing seien nur für sehr kleine Komponenten realistisch. Bei großen Codebasen sorge die kombinatorische Explosion dafür, dass das Vorhaben aussichtslos wird.

Marco hat dem zugestimmt und die Architektur beschrieben, die daraus folgt. Er arbeitete damals an einem Vortrag dazu, den er kurz nach unserem Gespräch auf dem phpday 2022 gehalten hat: How I sliced your app. Die Idee: Jedes Feature wird von einer kleinen, in sich geschlossenen Komponente abgebildet. Jede dieser Komponenten hat ihre eigene Branch Coverage, ihren eigenen Mutation Score und ihre vollständige Typisierung. Über allem liegt genau ein End-to-End-Test für den Happy Path. Abhängigkeiten gehören dann nicht mehr der ganzen Anwendung, sondern einzelnen Features.

Damit dreht sich die Reihenfolge um. Wer über Mutation Testing sprechen will, muss vorher über Komponentengrenzen gesprochen haben.

Fazit

Marco hat das Gespräch selbst am besten zusammengefasst. Mutation Testing sei ein Super-Set von Branch Coverage und hole etwas mehr heraus. Isoliert betrachtet bringt es allerdings wenig. Es funktioniert als oberste Schicht über kleinen Komponenten, strikter Typisierung und einer erreichten Branch Coverage, und es funktioniert deutlich besser, wenn statische Analyse die sinnlosen Mutationen vorher aussortiert. Dieser letzte Teil steckt inzwischen im Werkzeug selbst und hat 2022 noch ein Plugin gebraucht.

Für mich ist das längst keine fremde Position mehr. Ich arbeite mittlerweile schon länger genau so, wie Marco es damals beschrieben hat. Dabei ist Mutation Testing nur eine Schicht von mehreren, und jede dieser Schichten fängt andere Fehler ab als die übrigen. Wie sie zusammenspielen und warum keine davon für sich allein genügt, habe ich in Alles, was wir haben aufgeschrieben.

Vier Jahre später bleibt für mich vor allem das: Architektur ist wichtiger als Werkzeuge, und keine Metrik nimmt dir die Entscheidung ab, was gut genug ist. Auf 100 % zu zielen lohnt sich nicht. Ein Schwellenwert, den dein Team für angemessen hält, als Gate in der CI ist der bessere Weg. Was danach kommt, ist Handarbeit: Code kürzen, Tests schärfen, Typen präzisieren.

Vollständig absichern lässt sich Software damit nicht, und das war auch die Stimmung, in der unser Gespräch damals endete. Es geht um bewusste Entscheidungen und nicht um perfekte Zahlen. Die Werkzeuge, die wir dafür haben, sind unfertig, und sie machen unsere Arbeit trotzdem besser.