mit Stryker prüfen, ob eure Tests wirklich etwas testen
Hundert Prozent Code-Coverage bedeutet nur, dass jede Zeile einmal ausgeführt wurde, nicht dass ein Test einen echten Fehler auch bemerken würde. Mutation Testing dreht diese Frage um: Stryker verändert gezielt einzelne Codezeilen zu kleinen Bugs und prüft, ob eure Tests diese Mutanten tatsächlich erkennen und fehlschlagen, statt stillschweigend grün zu bleiben.
Inhaltsverzeichnis
- 1. Warum Code-Coverage nicht genügt
- 2. Das Konzept: Mutanten, Killed und Survived
- 3. Stryker installieren und den ersten Lauf starten
- 4. Mutatoren verstehen: Welche Änderungen Stryker erzeugt
- 5. Den Mutation Score richtig interpretieren
- 6. Überlebende Mutanten gezielt beheben
- 7. Performance: Laufzeit und Incremental Mode
- 8. Mutation Testing in der CI-Pipeline verankern
- 9. Mutation Testing im Vergleich zu Code-Coverage
- 10. Zusammenfassung
- 11. FAQ
1. Warum Code-Coverage nicht genügt
Code-Coverage-Werkzeuge messen, welcher Anteil des Codes während eines Testlaufs ausgeführt wurde. Eine Coverage von hundert Prozent bedeutet lediglich, dass jede Zeile mindestens einmal durchlaufen wurde, sagt aber nichts darüber aus, ob die Tests dabei tatsächlich das richtige Verhalten überprüft haben. Ein Test, der eine Funktion aufruft, aber niemals das Ergebnis mit expect() prüft, erzeugt volle Coverage und trotzdem null Aussagekraft. Genau diese Lücke schließt Mutation Testing.
Mutation Testing für JavaScript stellt eine andere Frage: Würde die Testsuite überhaupt bemerken, wenn der Code kaputt wäre? Um das zu beantworten, verändert Stryker Mutator gezielt einzelne Codezeilen, ein > wird zu >=, ein true wird zu false, ein + wird zu -, und führt die gesamte Testsuite gegen jede dieser kleinen, künstlich eingebauten Fehler, den sogenannten Mutanten, erneut aus. Schlägt mindestens ein Test fehl, wurde der Mutant getötet, die Testsuite hat den Fehler erkannt. Bleiben alle Tests grün, hat der Mutant überlebt, und genau das ist ein Alarmsignal.
Der praktische Nutzen von Mutation Testing zeigt sich vor allem in Projekten, die stolz auf hohe Coverage-Zahlen sind, aber trotzdem regelmäßig Bugs in Produktion erleben. Stryker deckt in solchen Fällen häufig auf, dass viele Tests zwar Code ausführen, aber keine oder nur schwache Assertions enthalten, und liefert damit eine deutlich ehrlichere Metrik für die tatsächliche Wirksamkeit einer Testsuite als reine Coverage-Prozentzahlen.
2. Das Konzept: Mutanten, Killed und Survived
Ein Mutant ist eine minimal veränderte Version des Quellcodes, erzeugt durch einen sogenannten Mutator, eine Regel, die einen bestimmten Codeausdruck systematisch verändert. Stryker erzeugt für jede mögliche Mutation an jeder relevanten Stelle im Code einen eigenen Mutanten und führt für jeden einzelnen die komplette Testsuite aus. Bei einer Funktion mit einem Vergleichsoperator entsteht so mindestens ein Mutant, der > durch >= ersetzt, ein zweiter, der ihn durch < ersetzt, und so weiter.
Für jeden Mutanten gibt es beim Mutation Testing genau zwei mögliche Ergebnisse. Killed bedeutet, mindestens ein Test ist fehlgeschlagen, als der Mutant aktiv war, die Testsuite hat den eingebauten Fehler also erfolgreich erkannt. Survived bedeutet, alle Tests sind trotz des Fehlers weiterhin grün geblieben, was bedeutet, dass kein Test diesen spezifischen Aspekt des Verhaltens tatsächlich überprüft. Ein dritter, selteneren Fall ist No Coverage, bei dem der mutierte Code überhaupt nicht von einem Test erreicht wurde, was auf eine Coverage-Lücke statt auf einen fehlenden Assertion-Fehler hinweist.
3. Stryker installieren und den ersten Lauf starten
Die Installation erfolgt über den offiziellen Stryker-Initialisierer, der ein zu den bestehenden Projektabhängigkeiten passendes Setup vorschlägt, inklusive Test-Runner-Erkennung für Vitest, Jest oder Mocha. Nach der Konfiguration führt npx stryker run den vollständigen Mutation Testing Lauf aus und erzeugt am Ende einen HTML-Report mit farblich markierten Mutanten direkt im Quellcode.
// npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner
// stryker.config.mjs
export default {
packageManager: 'npm',
reporters: ['html', 'clear-text', 'progress'],
testRunner: 'vitest',
coverageAnalysis: 'perTest',
mutate: [
'src/**/*.js',
'!src/**/*.test.js',
],
thresholds: {
high: 80,
low: 60,
break: 50, // fail the run if the mutation score drops below 50%
},
};
// Run: npx stryker run
// Output includes a summary like:
// 142 mutants tested, 118 killed, 19 survived, 5 no coverage
// Mutation score: 83.10%
Wichtig beim ersten Mutation Testing Lauf ist, nicht das gesamte Projekt auf einmal zu mutieren, besonders bei größeren Codebasen. Die mutate-Option in der Konfiguration erlaubt es, gezielt einzelne Verzeichnisse einzuschließen, etwa kritische Geschäftslogik, während generierte Dateien, Konfigurationsdateien oder rein deklarative Typen ausgeschlossen bleiben, weil sie ohnehin keine sinnvollen Testfälle erzeugen würden.
4. Mutatoren verstehen: Welche Änderungen Stryker erzeugt
Stryker bringt eine ganze Bibliothek eingebauter Mutatoren mit, die unterschiedliche Kategorien von Codeänderungen erzeugen. Arithmetic Operator Mutatoren ersetzen + durch - oder * durch /. Conditional Boundary Mutatoren verändern < zu <= und umgekehrt, eine der häufigsten Quellen echter Off-by-one-Fehler. Boolean Literal Mutatoren tauschen true gegen false. Und String Literal Mutatoren ersetzen einen konstanten String durch einen leeren String, um zu prüfen, ob überhaupt jemand den konkreten Textinhalt verifiziert.
Diese Bandbreite an Mutatoren ist der Grund, warum Mutation Testing systematisch Schwachstellen findet, die ein Mensch beim Review kaum entdecken würde. Ein Entwickler, der Code liest, sieht sofort, dass if (age >= 18) korrekt aussieht. Ob ein Test aber tatsächlich den Grenzfall age === 18 prüft, zeigt sich erst, wenn Stryker genau diesen Grenzwert mutiert und beobachtet, ob ein Test dabei fehlschlägt.
// src/discount.js — original implementation
export function calculateDiscount(age, isMember) {
if (age >= 65 && isMember) {
return 0.2; // 20% senior member discount
}
if (age >= 18) {
return 0.05; // 5% adult discount
}
return 0;
}
// A weak test suite with high coverage but a low mutation score
import { describe, it, expect } from 'vitest';
import { calculateDiscount } from './discount.js';
describe('calculateDiscount (weak suite)', () => {
it('returns a discount for an adult', () => {
expect(calculateDiscount(30, false)).toBeGreaterThan(0); // too loose!
});
});
// This test passes 100% coverage but would NOT kill a mutant that
// changes 0.05 to 0.5, or >= to >, because it never asserts the
// exact expected value. Stryker reports these mutants as "survived".
// A strong test suite that actually kills the relevant mutants
describe('calculateDiscount (strong suite)', () => {
it('gives no discount below 18', () => {
expect(calculateDiscount(17, false)).toBe(0);
});
it('gives exactly 5% at the boundary age of 18', () => {
expect(calculateDiscount(18, false)).toBe(0.05);
});
it('gives exactly 20% for senior members at 65', () => {
expect(calculateDiscount(65, true)).toBe(0.2);
});
});
5. Den Mutation Score richtig interpretieren
Der Mutation Score ist der Prozentsatz der getöteten Mutanten im Verhältnis zur Gesamtzahl der erzeugten Mutanten, ohne die als No Coverage markierten Fälle. Ein Score von achtzig Prozent bedeutet, dass von allen künstlich eingebauten Fehlern vier von fünf tatsächlich einen Testfehlschlag ausgelöst haben. Anders als bei Code-Coverage gibt es beim Mutation Testing keine allgemeingültige Zielgröße von hundert Prozent, weil manche Mutanten semantisch äquivalent zum Original sind und niemals getötet werden können, unabhängig davon, wie gut die Tests sind.
Ein solcher äquivalenter Mutant entsteht etwa, wenn eine Mutation ein totes Code-Fragment verändert, das ohnehin nie ausgeführt wird, oder wenn zwei mathematisch unterschiedliche, aber im konkreten Wertebereich identische Ausdrücke entstehen. Stryker kann äquivalente Mutanten nicht automatisch erkennen, weshalb ein realistisches Ziel für Mutation Testing meist zwischen siebzig und neunzig Prozent liegt, abhängig von der Komplexität und Kritikalität des jeweiligen Codes, nicht bei den vollen hundert Prozent.
6. Überlebende Mutanten gezielt beheben
Der HTML-Report von Stryker markiert jeden überlebenden Mutanten direkt im Quellcode mit der genauen Zeile und der Art der Mutation. Der erste Schritt bei der Analyse eines Survivors ist immer die Frage, ob überhaupt ein Test existiert, der diesen Codepfad ausführt. Existiert kein Test, ist die Lücke offensichtlich, ein neuer Testfall mit einer präzisen Assertion muss ergänzt werden.
Komplexer wird es, wenn ein Test existiert, den Codepfad ausführt, aber trotzdem keine ausreichend präzise Assertion enthält, wie im Beispiel mit toBeGreaterThan(0) statt eines exakten Werts. Hier zeigt Mutation Testing seine eigentliche Stärke: Es macht sichtbar, dass ein Test zwar existiert und sogar den Code ausführt, aber inhaltlich zu schwach formuliert ist, um einen tatsächlichen Regressionsfehler zu erkennen. Die Korrektur besteht meist darin, die vage Assertion durch eine exakte zu ersetzen, die den erwarteten Wert konkret prüft, statt nur eine grobe Richtung zu bestätigen.
7. Performance: Laufzeit und Incremental Mode
Der offensichtliche Nachteil von Mutation Testing ist die Laufzeit. Für jeden einzelnen Mutanten muss die relevante Teilmenge der Testsuite erneut ausgeführt werden, was bei hunderten Mutanten schnell zu Laufzeiten von mehreren Minuten bis Stunden führt, deutlich länger als ein einzelner normaler Testlauf. Stryker begrenzt diesen Aufwand durch coverageAnalysis: 'perTest', das für jeden Mutanten nur die Tests ausführt, die den betroffenen Code tatsächlich abdecken, statt die komplette Suite gegen jeden einzelnen Mutanten laufen zu lassen.
Für große Projekte bietet Stryker zusätzlich den Incremental Mode, der bei jedem Lauf nur Mutanten in geändertem Code neu bewertet und die Ergebnisse unveränderter Dateien aus einem vorherigen Lauf wiederverwendet. In Kombination mit paralleler Ausführung über mehrere CPU-Kerne bringt das die Laufzeit von Mutation Testing auf ein Niveau, das sich auch in mittelgroßen CI-Pipelines regelmäßig einplanen lässt, statt es auf gelegentliche, manuell angestoßene Analysen zu beschränken.
# Incremental mode: only re-evaluate mutants in changed files
npx stryker run --incremental
# Concurrency: use available CPU cores for parallel test runs
npx stryker run --concurrency 4
# Combine both for fast, regular CI runs on pull requests
npx stryker run --incremental --concurrency 4
8. Mutation Testing in der CI-Pipeline verankern
Die thresholds-Konfiguration in Stryker erlaubt es, den CI-Lauf gezielt fehlschlagen zu lassen, wenn der Mutation Score einen definierten Schwellenwert unterschreitet. Das break-Threshold ist dabei die harte Grenze, unterhalb derer der Build als fehlgeschlagen markiert wird, während high und low im HTML-Report lediglich zur farblichen Einordnung dienen. Für Mutation Testing in der CI-Pipeline empfiehlt sich ein moderater Einstiegswert, der schrittweise erhöht wird, sobald die bestehende Testsuite entsprechend nachgebessert wurde.
Wegen der Laufzeit ist es in vielen Projekten sinnvoller, Mutation Testing nicht bei jedem Commit, sondern gezielt bei Pull Requests gegen den Hauptbranch oder in einem nächtlichen Job auszuführen, statt jede einzelne Änderung durch den vollständigen Mutationslauf zu blockieren. Kombiniert mit dem Incremental Mode lässt sich der Kompromiss zwischen Feedback-Geschwindigkeit und Testtiefe für das jeweilige Projekt individuell austarieren.
9. Mutation Testing im Vergleich zu Code-Coverage
Die folgende Tabelle stellt Mutation Testing der klassischen Code-Coverage-Messung gegenüber.
| Kriterium | Code-Coverage | Mutation Testing (Stryker) |
|---|---|---|
| Misst | Ausgeführte Codezeilen | Tatsächlich erkannte Fehler |
| Erkennt schwache Assertions | Nein | Ja, gezielt |
| Laufzeit | Sekunden bis Minuten | Minuten bis Stunden, ohne Optimierung |
| Zielwert | Oft nahe 100% angestrebt | 70 bis 90%, wegen äquivalenter Mutanten |
| Aussagekraft bei false confidence | Gering, täuscht leicht Sicherheit vor | Hoch, deckt genau diese Täuschung auf |
Code-Coverage und Mutation Testing schließen sich nicht gegenseitig aus, sondern ergänzen sich: Coverage ist eine schnelle, grobe Voraussetzung, während Mutation Testing die eigentliche Frage beantwortet, ob die vorhandenen Tests im Ernstfall auch tatsächlich anschlagen würden.
Mironsoft
Testqualität statt Coverage-Zahlen für Magento- und Hyvä-Projekte
Wisst ihr, ob eure Tests echte Bugs erkennen würden?
Wir führen Mutation Testing mit Stryker in eure kritische Geschäftslogik ein, analysieren überlebende Mutanten gemeinsam mit eurem Team und etablieren realistische Mutation-Score-Ziele in der CI-Pipeline.
Stryker-Einführung
Konfiguration, Mutatoren-Auswahl und erster Lauf für kritische Module
Survivor-Analyse
Überlebende Mutanten gemeinsam beheben und Assertions gezielt schärfen
CI-Integration
Incremental Mode und Thresholds für performante, regelmäßige Läufe
10. Zusammenfassung
Mutation Testing für JavaScript beantwortet eine Frage, die Code-Coverage nicht beantworten kann: Würde die Testsuite einen echten Fehler tatsächlich bemerken? Stryker Mutator erzeugt dafür systematisch kleine, künstliche Fehler im Code, sogenannte Mutanten, und führt die Testsuite gegen jeden Einzelnen aus. Getötete Mutanten bestätigen wirksame Tests, überlebende Mutanten decken schwache oder fehlende Assertions auf, die reine Coverage-Messung niemals sichtbar machen würde.
Ein realistischer Mutation Score liegt meist zwischen siebzig und neunzig Prozent, weil äquivalente Mutanten prinzipiell nicht tötbar sind. Performance-Optionen wie Incremental Mode und parallele Ausführung machen Mutation Testing auch für größere Codebasen praktikabel, während Thresholds in der CI-Pipeline verhindern, dass die Testqualität schleichend absinkt. Wer Mutation Testing gezielt auf kritische Geschäftslogik anwendet, gewinnt eine deutlich ehrlichere Einschätzung der tatsächlichen Testabdeckung als jede reine Prozentzahl.
Mutation Testing für JavaScript mit Stryker — Das Wichtigste auf einen Blick
Kernidee
Künstliche Mutanten in den Code einbauen und prüfen, ob Tests sie tatsächlich als Fehler erkennen.
Killed vs. Survived
Killed bestätigt wirksame Tests, Survived deckt schwache oder fehlende Assertions auf.
Realistisches Ziel
70 bis 90 Prozent Mutation Score, nicht 100 Prozent, wegen äquivalenter Mutanten.
Performance
coverageAnalysis: perTest, Incremental Mode und Parallelisierung für praktikable Laufzeiten.