Mutationstests sind negativ. TDD-Tests sind positiv.
Beide heißen Tests, aber sie behaupten Gegensätzliches. Ein TDD-Test sagt, was das System tun muss. Ein Mutationslauf kann nur melden, was Ihre Testsuite übersehen hat. Welches Vorzeichen Sie in der Hand halten, entscheidet, was mit dem Ergebnis zu tun ist.
Zwei Arten von Behauptung
Ein testgetriebener Test wird geschrieben, bevor das Verhalten existiert. Er scheitert, dann besteht er, und von da an ist er eine stehende Aussage: diese Eingabe muss zu diesem Ergebnis führen. Die Suite wächst zu einer lesbaren Spezifikation, und weil der Test zuerst entsteht, erzeugt er Druck auf das Design, denn Code, der schwer aufzurufen ist, ist schwer zu testen. Ein Mutationslauf tut etwas strukturell anderes. Er nimmt funktionierenden Code, verändert ihn absichtlich und lässt die Suite erneut laufen. Jedes Ergebnis ist negativ formuliert: diese Änderung blieb unbemerkt. Nichts in dieser Ausgabe sagt, wofür die Software da ist.
Eine grüne TDD-Suite ist eine Menge von Aussagen über Absichten. Ein hoher Mutation Score ist das Fehlen einer bestimmten Art von Gegenbeweis gegen Ihre Suite. Nur eines von beidem ist eine Spezifikation.
Der positive Test
- + Vor dem Code geschrieben, sodass die Anforderung in Worten existiert, bevor sie in Logik existiert
- + Scheitert zuerst, was der einzige billige Beweis dafür ist, dass der Test überhaupt scheitern kann
- + Benennt eine Regel und übersteht deshalb Refactorings: die Interna dürfen sich bewegen, die Behauptung nicht
- + Erzeugt Designdruck, solange das Design noch günstig zu ändern ist
- + Ist für die nächste Person lesbar, auch für den nächsten Agenten
Die negative Prüfung
- − Läuft nachträglich, auf Code und Tests, die schon existieren
- − Ein getöteter Mutant bringt keine neue Information; nur Überlebende tragen ein Signal
- − Ein Überlebender belegt Blindheit, niemals einen Fehler im Produkt
- − Sagt nichts über fehlende Anforderungen, nur über Unempfindlichkeit gegenüber den versuchten Änderungen
- − Erzeugt einen Bericht und eine Aufgabenliste, kein Artefakt, das jemand behält
Dasselbe Wort, zwei verschiedene Instrumente
| Der positive Test | Die negative Prüfung | |
|---|---|---|
| Was es behauptet | Das System muss sich so verhalten. | Ihre Suite hat diese Änderung nicht bemerkt. |
| Was grün beweist | Jedes bisher spezifizierte Verhalten ist implementiert. | Die Suite reagiert auf diese Operatorenmenge. Nichts über Korrektheit. |
| Was zurückbleibt | Tests und ein Design, das durch ihr Schreiben geformt wurde. | Ein Bericht. Der Wert muss entnommen werden, bevor er verfällt. |
| Wann es läuft | Fortlaufend, im Minutenrhythmus, während der Code entsteht. | In der CI: inkrementell auf geänderten Dateien oder als nächtlicher Durchlauf. |
| Kostenmodell | Stetig bezahlt und in die Entwicklung eingerechnet. | Mutanten mal relevante Testlaufzeit. Wächst mit beidem. |
| Wirkung aufs Design | Testbarkeitsdruck. Ein unhandliches Design schmerzt sofort. | Keine. Es bewertet, was schon existiert. |
| Wie das Scheitern kommt | Rot, absichtlich, von Ihnen verfasst, Schritt für Schritt. | Ein Überlebender, den niemand geschrieben hat und der nun gedeutet werden muss. |
Derselbe Überlebende, zwei Antworten
Hier hört das Vorzeichen auf, Philosophie zu sein. Ein Mutationsbericht liefert eine Stelle; was Sie danach schreiben, entscheidet, ob die Suite stärker oder nur steifer wird.
Der Code und der Mutant, der überlebt
jsDas Entfernen von .trim() ist eine übliche Methodenaufruf-Mutation. Sie überlebt immer dann, wenn keine Fixture umgebende Leerzeichen enthält, und das gilt für die meisten Fixtures.
// importer.js
const normalise = (value) => value.trim().toLowerCase();
export function importRows(rows) {
return rows.map((row) => ({ email: normalise(row.email) }));
}
// Surviving mutant: normalise() with .trim() removed.
// The suite passes either way, so the report flags it. Den Mutanten töten und die Suite verkoppeln
jsDieser Test tötet den Überlebenden. Er friert aber auch die aktuelle Implementierung ein: normalise muss erreichbar bleiben und genau so oft aufgerufen werden. Umbenennen, inlinen oder hinter eine Grenze verschieben, und der Test bricht, obwohl sich das Verhalten überhaupt nicht geändert hat.
it('calls normalise once per row', () => {
const spy = vi.spyOn(internals, 'normalise');
importRows([{ email: ' Ada@Example.COM ' }]);
expect(spy).toHaveBeenCalledTimes(1);
});
// Green. Mutant dead. Score up.
// Nothing here states what an imported email address should look like. Die Frage beantworten, die der Überlebende gestellt hat
jsDerselbe Mutant, derselbe Abschuss, aber die Behauptung ist eine Regel, die auch ein Produktverantwortlicher lesen könnte. Sie geht durch die öffentliche Funktion, die Interna bleiben also beweglich, und sie erklärt sich in sechs Monaten in der Fehlermeldung selbst.
it('trims and lower-cases every imported email address', () => {
expect(importRows([{ email: ' Ada@Example.COM ' }]))
.toEqual([{ email: 'ada@example.com' }]);
});
// Same mutant dead, but the suite gained a specification
// instead of a snapshot of today's call graph. Das Audit billig genug halten, um es zu behalten
jsonCoverage-Analyse pro Test ist der größte Performance-Hebel: es laufen nur die Tests, die den jeweiligen Mutanten überhaupt erreichen. Der inkrementelle Modus hält einen Pull-Request-Lauf im Minutenbereich. Die break-Schwelle ist ein Boden gegen Rückschritte, kein Ziel zum Jagen.
{
"testRunner": "vitest",
"coverageAnalysis": "perTest",
"incremental": true,
"mutate": ["src/**/*.js", "!src/**/*.test.js"],
"thresholds": { "high": 80, "low": 60, "break": 60 }
} Wo das Negative giftig wird
Dem Score nachjagen
Sobald der Mutation Score ein Ziel ist, entstehen Tests, die Mutanten töten, statt Anforderungen auszusprechen. Sie kommen durchs Review, weil sie grün sind, und sie sind genau die, die beim nächsten Refactoring brechen, obwohl sich am Verhalten nichts geändert hat.
Äquivalente Mutanten bekämpfen
Manche Mutationen ändern den Code, ohne beobachtbares Verhalten zu ändern. Kein ehrlicher Test kann sie töten. Dokumentieren, ausschließen, weitermachen. Zeit hier kauft eine Zahl, kein Vertrauen.
Die Mutation statt der Regel behaupten
Wenn Sie die Anforderung nicht benennen können, die ein neuer Test schützt, haben Sie einen Änderungsdetektor geschrieben. Er meldet künftig jede Bearbeitung als Fehlschlag und bringt dem Team bei, die Testausgabe nicht mehr zu lesen.
Alles jedes Mal laufen lassen
Ein vollständiger Durchlauf auf einer großen Codebasis ist ein Nachtjob, kein Gate für Pull Requests. Richten Sie den PR-Lauf mit Coverage-Analyse pro Test auf die geänderten Dateien und halten Sie den vollständigen Lauf aus dem kritischen Pfad heraus.
Jeden Überlebenden in eine positive Aussage verwandeln
Den Überlebenden als Frage lesen
- - Welches Verhalten müsste gelten, damit diese Änderung etwas kaputt macht?
- - Welche Anforderung würde jetzt scheitern, wenn sie jemand aufgeschrieben hätte?
- - Wer weiter unten in der Kette würde merken, wenn diese Mutation ausgeliefert würde?
Die Antwort schreiben, nicht den Abschuss
- - Den Test nach der Regel benennen, nie nach dem Mutanten oder der Zeilennummer
- - Über die öffentliche Oberfläche prüfen, damit die Interna beweglich bleiben
- - Wenn die Regel nur durch Offenlegen von Interna erreichbar ist, ist das ein Designbefund, kein Testbefund
Oder den Code löschen
- - Wenn keine Anforderung benennbar ist, hängt tatsächlich nichts von dem Verhalten ab
- - Ein Überlebender in unspezifiziertem Code ist oft eine tote Anforderung und kein fehlender Test
- - Löschen ist der billigste mögliche Abschuss und macht das nächste Audit schneller
Das eine schreibt die Spezifikation, das andere prüft sie
Mutationstests sind kein besseres TDD und kein Ersatz dafür. Sie erzeugen keine Tests, kein Design und keine Aussage über Absichten. Was sie erzeugen, ist eine Liste von Stellen, an denen Ihre Spezifikation unempfindlicher ist als angenommen, und das ist wirklich wertvoll und wirklich etwas anderes.
Halten Sie die Arbeitsteilung also explizit. Tests entstehen durch TDD, als positive Behauptungen über Verhalten. Mutationsläufe finden regelmäßig als Audit dieser Behauptungen statt. Und kein Überlebender wandert direkt in einen Test: erst in eine Anforderung übersetzen oder den Code löschen, in dem er sitzt. Ein negativer Befund wird nur dann dauerhafter Wert, wenn jemand ihn in eine positive Aussage darüber verwandelt, wofür die Software da ist.
Verwandte Engineering-Artikel
Coverage und Komplexität haben ihre eigene Metrik, und es gibt inzwischen einen Agenten-Workflow, der Hardening zu einer eigenen Rolle macht.
Die CRAP-Metrik: Komplexität und Coverage in einer Zahl
Wie die CRAP-Formel Komplexität mit Coverage verbindet, was jeder Wert verlangt und wie man darauf gatet, ohne ein Aufräumprojekt zu starten.
SwarmForge: Was der Agentenschwarm von Uncle Bob richtig macht
Ein Review der Rollenaufteilung, Worktree-Isolation und des Handoff-Protokolls von SwarmForge und was davon auf jedes Team übertragbar ist.
FAQ
Sind Mutationstests besser als Code Coverage?
Sie beantworten eine stärkere Frage zu einem viel höheren Preis. Coverage zählt, ob eine Zeile ausgeführt wurde; Mutation fragt, ob überhaupt etwas geprüft wurde. Nutzen Sie Coverage als billiges Dauergate und Mutationstests als periodisches Audit für Code, auf den es ankommt.
Welchen Mutation Score sollten wir anstreben?
Werte über etwa 80 Prozent gelten üblicherweise als stark und 60 bis 80 Prozent als brauchbar mit echten Lücken, aber die Zahl bedeutet nur pro Modul etwas. Nützlicher ist die Regel, dass es auf geänderten Dateien keinen Rückschritt geben darf, mit einer break-Schwelle, die das Absinken stoppt.
Ersetzen Mutationstests TDD?
Nein. Mutationstests schreiben nichts und spezifizieren nichts. Sie bewerten die Suite, die Sie schon haben, und jeder Befund braucht weiterhin eine menschliche Entscheidung darüber, welche Anforderung er impliziert.
Welche Werkzeuge nutzen Teams?
PIT ist die etablierte Wahl auf der JVM, Stryker Mutator deckt JavaScript, TypeScript, C# und Scala ab, und in Python sind mutmut und cosmic-ray die üblichen Optionen. Alle können einen Lauf auf geänderte Dateien beschränken, und genau das macht die Praxis erschwinglich.
Wo gehören Mutationstests in die CI?
Inkrementell in Pull Requests, beschränkt auf geänderte Dateien mit Coverage-Analyse pro Test, plus ein vollständiger Durchlauf nach Zeitplan. Melden Sie Überlebende als Review-Punkte statt als Build-Fehler und gaten Sie nur darauf, dass der Score nicht zurückgeht.