Zum Inhalt springen

Mutation-Testing erklärt

Spec: ISO/IEC/IEEE 29119-4 Spec: PHPUnit Evidence: Test-backed

Line-Coverage zeigt Ihnen, dass eine Zeile während der Testsuite ausgeführt wurde. Sie zeigt Ihnen nicht, dass irgendein Test fehlgeschlagen wäre, wenn diese Zeile falsch wäre. Mutation-Testing schließt diese Lücke, indem es den Code bewusst beschädigt und prüft, ob die Tests es bemerken. Diese Seite erklärt, was ein Mutationsscore bedeutet und wie NextPDF ihn als Diagnosewerkzeug nutzt, nicht als Trophäe.

Coverage ist im Testing die vertrauteste Metrik und zugleich eine der irreführendsten. Ein Test, der eine Methode aufruft und nichts prüft, führt jede Zeile darin aus — perfekte Coverage, null Erkennung. Die Normliteratur stellt klar, dass die Ordnung zwischen Coverage-Kriterien keinen Hinweis auf deren Fähigkeit gibt, Fehler aufzudecken. Diese Fähigkeit ist die Eigenschaft, die sie als Testwirksamkeit bezeichnet (ISO/IEC/IEEE 29119-4, §C.2.4). Ein Coverage-Prozentsatz und eine Garantie für das Auffinden von Fehlern sind unterschiedliche Aussagen.

Für eine PDF-Engine ist das nicht akademisch. Eine Byte-Range-Prüfung einer Signatur, ein Cross-Reference-Offset, ein Encoding-Zweig — Tests können all dies vollständig „abdecken”, ohne jemals den Wert zu prüfen, auf den es ankommt. Eine grüne Suite mit schwachen Tests ist schlimmer als eine ehrliche Lücke, weil sie aktiv davon abhält, dass irgendjemand genauer hinsieht.

  • Mutation-Testing nimmt Tausende kleiner, bewusster Änderungen (Mutanten) am Quellcode vor — ändert ein < zu <=, ein + zu -, einen return-Wert — und führt die Tests jeweils erneut dagegen aus.
  • Schlägt ein Test bei einem Mutanten fehl, ist der Mutant getötet: irgendein Test hat dieses Verhalten tatsächlich geprüft. Bestehen alle Tests weiterhin, ist der Mutant entkommen: das Verhalten wurde ausgeführt, aber nie geprüft.
  • Der Mutation Score Indicator (MSI) ist, grob gesagt, getötete Mutanten geteilt durch die Gesamtzahl der nicht-äquivalenten Mutanten. Er misst, ob Ihre Tests Änderungen erkennen, nicht, ob sie den Code ausführen.
  • Manche Mutanten sind äquivalent — sie können das beobachtbare Verhalten nicht ändern, sodass kein Test sie töten kann. Sie als Fehlschläge zu zählen, wäre unehrlich. NextPDF beweist sie und verbucht sie in einem Ledger, statt sie informell abzutun.
  • NextPDF nutzt den MSI, um schwache Tests aufzuspüren und zu stärken. Er ist ein Diagnose-Gate in der Continuous Integration, keine Marketingzahl.

Mutation läuft mit dem Infection-Mutator gegen die Engine. Er ist für den Produktions-Quellbaum konfiguriert, mit aktivierten Mutator-Familien für Arithmetik, Boolean, Konditionsgrenzen, Gleichheit, Rückgabewerte und Entfernung — genau die Operatoren, die „ausgeführte, aber ungeprüfte” Logik aufdecken. Der Ablauf ist mechanisch:

  1. Start green The suite must pass before mutation begins.
  2. Mutate Apply one small, deliberate change to the source.
  3. Re-run Run the tests that cover the mutated line.
  4. Killed A test failed — the behaviour is genuinely asserted.
  5. Escaped All tests still pass — a weak spot to strengthen.
  6. Equivalent No test can kill it because behaviour is unchanged — proven and ledgered, not scored as a miss.
Die Mutation-Testing-Schleife, die NextPDF ausführt: grüne Tests nehmen, einen Mutanten erzeugen, die abdeckenden Tests erneut ausführen und den Mutanten klassifizieren als getötet (ein Test hat ihn erwischt), entkommen (eine zu behebende Lücke mit Coverage, aber ohne Assertion) oder als äquivalent bewiesen (kein Test kann ihn töten; verbucht im Ledger, nicht gegen den Score gerechnet).

Zwei Design-Entscheidungen machen die Zahl vertrauenswürdig. Erstens ist der Score als Gate verdrahtet. Die Continuous Integration erzwingt einen Mindest-MSI (und einen Mindest-Covered-MSI) und führt eine diff-begrenzte Variante auf geänderten Zeilen aus. Dadurch fällt eine Änderung, die Code, aber keine echten Assertions hinzufügt, schon im Review auf und nicht erst später. Zweitens rechnet NextPDF unbequeme Mutanten nicht stillschweigend heraus. Mutanten, die wirklich semantisch äquivalent sind — zum Beispiel !== gegenüber !=, wenn die strikte Typisierung garantiert, dass beide Operanden denselben Typ haben — werden in einem Mutation-Ledger mit einem expliziten Äquivalenz-Beweistest erfasst. Dadurch spiegelt die Anzahl der entkommenen Mutanten echte Lücken wider, keine Buchführung. PHPStan Level 10 plus strict_types plus typisierte Properties machen diese Äquivalenzbeweise stichhaltig.

Evidence: Test-backed Mutation-Testing ist in der Engine für die Produktions-Quellverzeichnisse mit Mutator-Familien konfiguriert, die ungeprüftes Verhalten aufdecken. Es wird als Continuous-Integration-Gate erzwungen, mit einem Mindest-MSI und einer diff-begrenzten Variante. Es ist eine Build-Prüfung, kein nachträglicher Gedanke.

Evidence: Test-backed Das Problem äquivalenter Mutanten wird ehrlich behandelt. Semantisch äquivalente Mutanten werden klassifiziert und durch gezielte Äquivalenz-Beweistests in einem Mutation-Ledger gestützt, wobei die Stichhaltigkeit jedes Beweises auf PHPStan Level 10 plus strikter Typisierung beruht. Die Anzahl der entkommenen Mutanten repräsentiert daher echtes unentdecktes Verhalten, kein untötbares Rauschen, das den Score künstlich schlechter aussehen lässt.

Evidence: Standard-backed Mutation ist eine anerkannte Technik, keine Erfindung von NextPDF. Spec: ISO/IEC/IEEE 29119-4, §B.2.4 beschreibt das Anwenden generischer Mutationen auf Elemente einer Spezifikation, um daraus spezifische Mutationen für Tests abzuleiten. Die Technik ist überhaupt nötig, weil dieselbe Norm feststellt, dass die Subsumtionsordnung der Coverage-Kriterien diese nicht nach ihrer Fähigkeit ordnet, Fehler aufzudecken (ISO/IEC/IEEE 29119-4, §C.2.4).

Evidence: Standard-backed Coverage selbst ist wohldefiniert und in ihrer Aussagekraft begrenzt. Spec: PHPUnit unterscheidet zwischen Line-, Branch- und Path- Coverage. Line-Coverage hält nur fest, dass eine ausführbare Zeile ausgeführt wurde. Wer diese Definition kennt, macht ihre Unzulänglichkeit offensichtlich.

Es kommt nicht auf den Befehl an — sondern darauf, was Ihnen ein entkommener Mutant verrät:

<?php
declare(strict_types=1);
final class ByteRange
{
// Suppose the production guard is:
// if ($offset < 0) { throw new InvalidByteRange(); }
public function assertNonNegative(int $offset): void
{
if ($offset < 0) {
throw new InvalidByteRange('offset must be >= 0');
}
}
}
// A test that EXECUTES this line but does not assert the boundary:
// $byteRange->assertNonNegative(5); // no exception expected, none asserted
// gives 100% line coverage of assertNonNegative().
//
// Mutation flips `< 0` to `<= 0`. Behaviour now differs ONLY at $offset === 0.
// If no test passes 0 and asserts what happens, every test still passes:
// the mutant ESCAPED. Coverage said "tested"; mutation said "the boundary
// is unasserted". The fix is a test that pins offset === 0, not a higher
// target.
//
// composer mutation:diff → mutate only changed lines, enforce min MSI
// composer mutation:full → full-tree mutation gate

Genau darin liegt das Wertversprechen dieses entkommenen Mutanten. Er hat eine echte, konkrete, fehlende Assertion lokalisiert, die ein Coverage-Bericht als vollständig getestet eingestuft hatte.

Das häufigste Missverständnis ist, dass der Mutationsscore eine Note sei, die es zu maximieren gelte. Ein sehr hoher MSI, der erreicht wird, indem man Tests schreibt, um Mutanten zu töten, ist genauso hohl wie eine hohe Coverage, die man durch Methodenaufrufe ohne Assertion erreicht. Die Metrik wurde manipuliert und misst keine Erkennung mehr. NextPDF nutzt den MSI, um schwache Tests aufzuspüren. Das Ergebnis ist eine bessere Assertion; Selbstdarstellung ist ausdrücklich nicht der Zweck.

Das zweite Missverständnis ist, dass jeder überlebende Mutant ein Mangel in den Tests sei. Manche Mutanten sind wirklich äquivalent und können von keinem Test getötet werden, weil sie das beobachtbare Verhalten nicht ändern. Sie als Fehlschläge zu behandeln, ergibt einen unehrlichen, künstlich niedrigen Score und gewöhnt die Leute daran, den Bericht zu ignorieren. NextPDFs Antwort darauf ist, Äquivalenz explizit zu beweisen und im Ledger zu verbuchen, statt sie stillschweigend zu unterdrücken oder vorzugeben, die Zahl sei schlechter, als sie ist.

Mutation-Testing misst, ob Tests injizierte Änderungen erkennen. Es beweist nicht, dass der Code korrekt ist. Es misst weder Performance noch Konformität. Es kann einen wirklich äquivalenten Mutanten nicht töten. Der aktuelle Mutationsscore, der geltende Mindest-MSI-Schwellenwert, die Anzahl der im Ledger verbuchten Äquivalente und jede Coverage-Kennzahl sind laufend aktualisierte Qualitätssignale, die aus Continuous-Integration-Artefakten erzeugt und mit dem Build veröffentlicht werden. Sie fehlen hier bewusst, weil eine in den Fließtext eingefügte Zahl veralten und zu einer kleinen Lüge werden würde. Die einzige stabile Tatsache, die diese Seite nennt, ist PHPStan Level 10, und das ist eine Konfigurationseigenschaft, die die Äquivalenzbeweise stützt, keine Messung.

Die Mutator-Auswahl, die Schwellenwerte und die Ledger-Richtlinie liegen in der Mutation-Konfiguration der Engine und können sich weiterentwickeln. Diese Konfiguration ist maßgeblich, falls sie jemals von dieser Seite abweicht. Über die Testwirksamkeit anderer Bibliotheken wird hier keine Aussage getroffen.

  • Die NextPDF-Testpyramide — die fünf Ebenen, deren Tests das Mutation-Testing auf echte Fehlererkennung prüft.
  • Strikte Typen, überall — wie PHPStan Level 10 und strikte Typisierung die Beweise für äquivalente Mutanten stichhaltig machen.
  • Golden-File-Testing — eine weitere Ebene, deren Erkennungskraft das Mutation-Testing validieren kann.
  • Mutant — eine einzelne kleine, bewusst gesetzte Änderung am Quellcode, mit der getestet wird, ob die Testsuite diese Änderung bemerken würde.
  • Getöteter Mutant — ein Mutant, der mindestens einen Test zum Fehlschlagen gebracht hat; das Verhalten wurde tatsächlich geprüft.
  • Entkommener Mutant — ein Mutant, bei dem alle Tests weiterhin bestanden. Das Verhalten wurde ausgeführt, aber nie geprüft — eine zu behebende Schwachstelle.
  • Äquivalenter Mutant — ein Mutant, der das beobachtbare Verhalten nicht ändern kann, sodass kein Test ihn töten kann. NextPDF beweist solche Mutanten und verbucht sie im Ledger.
  • MSI (Mutation Score Indicator) — grob gesagt, getötete Mutanten geteilt durch die Gesamtzahl der nicht-äquivalenten Mutanten; ein Maß für Erkennung, nicht für bloße Ausführung.
  • Line-Coverage — eine Metrik, die nur festhält, dass eine ausführbare Zeile während der Suite ausgeführt wurde; von PHPUnit definiert und für sich allein unzureichend.