Sind Sie sicher, dass Ihre Tests wirklich funktionieren?
\n\nStellen Sie sich vor: Sie haben 95% Codeabdeckung, alle Tests sind grün, CI/CD läuft fehlerfrei. Das Team ist zufrieden, der Manager ist ruhig. Aber eines Tages taucht ein Bug in der Produktion auf. Sie öffnen den Code, schauen sich die Tests an und verstehen: Sie prüfen lediglich, ob die Funktion eine Zahl zurückgibt, aber nicht, welchen Wert sie hat. Oder schlimmer noch – der Test ist so geschrieben, dass er unter allen Umständen bestanden wird.
\n\nKommt Ihnen das bekannt vor? Willkommen in einer Welt, in der der Prozentsatz der Abdeckung nur eine Zahl ist, keine Garantie für Qualität. Hier kommt Mutationstests (Mutation Testing) ins Spiel – eine Methode, die Ihre Tests wirklich leiden lässt und ihre Stichhaltigkeit beweisen muss.
\n\nIn diesem Artikel werden wir uns ansehen, wie Sie Mutation Testing in ein TypeScript-Projekt mit Stryker integrieren und warum dies Ihr neues Lieblingswerkzeug (oder eine neue Quelle von Kopfschmerzen) werden könnte.
\n\nWas ist Mutationstests in einfachen Worten?
\n\nMutationstests bedeuten, dass Sie Ihren Code nehmen, absichtlich kleine Fehler (Mutationen) einbauen und prüfen, ob Ihre Tests fehlschlagen. Wenn die Tests nicht fehlschlagen, testen Sie falsch. Wenn sie fehlschlagen, haben Sie gute Arbeit geleistet, aber der Mutant wurde getötet.
\n\nKlingt das nach Sadismus? Vielleicht. Aber es funktioniert. Stellen Sie sich vor, Sie wären ein QA-Ingenieur, der heimlich in den Code geschlüpft ist und === durch ==, > durch < ersetzt oder einen Funktionsaufruf entfernt hat. Ein guter Test muss das bemerken. Ein schlechter Test wird es übersehen.
In TypeScript mit seiner strengen Typisierung sind Mutationstests besonders nützlich: Sie erkennen Fälle, in denen die Typen korrekt sind, die Logik jedoch nicht.
\n\nWarum ist die normale Codeabdeckung (Code Coverage) eine Illusion?
\n\nAngenommen, Sie haben eine Funktion:
\n\nfunction calculateDiscount(price: number, isPremium: boolean): number {\n if (isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\nUnd einen Test:
\n\ntest('returns number', () => {\n const result = calculateDiscount(100, true);\n expect(typeof result).toBe('number');\n});\n\nAbdeckung? 100%. Test? Bestanden. Was aber, wenn wir price * 0.8 durch price * 0.5 ersetzen? Der Test wird trotzdem bestanden – er prüft ja nur den Typ, nicht den Wert. Das ist ein trügerisches Sicherheitsgefühl.
Mutationstests würden eine solche Mutation aufdecken und sagen: „Hey, dein Test hat nicht bemerkt, dass ich den Rabatt geändert habe!“. Und Sie müssten eine ordentliche Prüfung nachschreiben.
\n\nWie funktioniert Mutation Testing in TypeScript: Schritt für Schritt
\n\nDer gesamte Prozess lässt sich in drei Phasen unterteilen:
\n\n- \n
- Generierung von Mutanten – Das Tool kopiert Ihren Code und nimmt jeweils eine Änderung vor. Zum Beispiel ersetzt es
&&durch||oder entfernt einen Methodenaufruf. \n - Ausführung der Tests – Für jeden Mutanten werden Ihre Tests ausgeführt. \n
- Analyse der Ergebnisse – Wenn die Tests fehlschlagen, ist der Mutant „getötet“. Wenn nicht, hat er „überlebt“, und das ist eine Risikozone. \n
Die wichtigste Kennzahl ist der Mutation Score (Prozentsatz der getöteten Mutanten). 100% sind ideal, aber in der Praxis sind 80-90% bereits hervorragend.
\n\nDas Werkzeug Nr. 1 für TypeScript: Stryker
\n\nIn der Welt von TypeScript und JavaScript ist der König der Mutationstests Stryker (StrykerJS). Er ist schnell, kann mit Jest, Vitest, Mocha und sogar mit Browser-Tests arbeiten. Die Einrichtung dauert 5 Minuten.
\n\nInstallation:
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\nErstellen Sie eine Konfigurationsdatei stryker.conf.js:
// stryker.conf.js\nexport default {\n mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], // was mutiert werden soll\n testRunner: 'jest',\n reporters: ['html', 'clear-text', 'progress'],\n jest: {\n projectType: 'custom',\n configFile: 'jest.config.ts',\n },\n concurrency: 4, // wie viele Mutanten