Ihre Tests lügen, und Sie wissen es nicht
\n\nStellen Sie sich vor: Sie haben Unit-Tests geschrieben, die Code Coverage beträgt 95%. Der Chef ist zufrieden, die CI/CD-Pipeline ist grün. Aber am Freitagabend taucht ein Bug in der Produktion auf. Kommt Ihnen das bekannt vor?
\n\nDas Problem ist, dass die klassische Codeabdeckung eine Illusion ist. Sie zeigt, welche Codezeilen ausgeführt wurden, beantwortet aber nicht die entscheidende Frage: Überprüfen die Tests tatsächlich die Logik? Genau hier kommt Mutation Testing ins Spiel – eine Methode, die Ihren Code gnadenlos "kaputt macht", um zu prüfen, wie gut Ihre Tests sind.
\n\nIn diesem Artikel werden wir uns ansehen, was Mutationstests in Java sind, wie sie funktionieren, und zeigen reale Beispiele mit dem Tool PIT (Pitest). Sie erfahren, wie Sie Ihre Tests von einem "Häkchen im Bericht" in einen echten Schutz vor Bugs verwandeln.
\n\nWas ist Mutation Testing und warum ist es wichtig?
\n\nMutation Testing (oder Mutationstests) ist eine Methode zur Bewertung der Testqualität, bei der kleine, absichtlich falsche Änderungen (sogenannte Mutationen) in den Quellcode eingefügt werden. Anschließend werden Ihre Tests ausgeführt. Wenn mindestens ein Test fehlschlägt – der Mutant wurde "getötet", der Test hat funktioniert. Wenn die Tests jedoch bestehen – der Mutant "überlebt", und das ist ein Alarmzeichen.
\n\nEine einfache Metapher: Sie überprüfen, wie gut Ihre Alarmanlage Diebe fängt. Sie legen einen "Dieb" (Mutanten) hinein und schauen, ob der Alarm (Test) auslöst. Wenn der Dieb ruhig verschwindet – taugt die Alarmanlage nichts.
\n\nWarum ist Code Coverage ein schlechter Indikator?
\n\nStellen Sie sich diesen Code vor:
\n\npublic boolean isAdult(int age) {\n if (age >= 18) {\n return true;\n }\n return false;\n}\n\nUnd den Test:
\n\n@Test\nvoid testIsAdult() {\n assertTrue(isAdult(18));\n}\n\nDie Codeabdeckung würde 100% anzeigen (beide if-else-Zweige ausgeführt?). Nein! Es wurde nur der true-Zweig ausgeführt. Aber der Test ist grün. Stellen Sie sich nun vor, jemand ändert versehentlich die Bedingung auf age > 21. Der Test würde trotzdem bestehen, weil 18 > 21 false ist, assertTrue aber true erwartet. Der Bug würde in die Produktion gelangen.
Mutationstests würden eine solche Situation sofort erkennen, indem sie einen Mutanten erzeugen, der >= in > ändert oder die Bedingung umkehrt.
Wie funktioniert Mutation Testing: Theorie einfach erklärt
\n\nDer Prozess besteht aus drei Schritten:
\n\n- \n
- Generierung von Mutanten. Das Tool (z. B. PIT) erstellt mehrere Versionen Ihres Codes mit kleinen Änderungen: Es ändert Operatoren, entfernt Methodenaufrufe, kehrt Bedingungen um. \n
- Ausführen der Tests. Für jeden Mutanten werden Ihre Unit-Tests ausgeführt. \n
- Analyse der Ergebnisse. Wenn ein Test fehlschlägt – der Mutant ist getötet. Wenn er besteht – lebt er. Der Prozentsatz der getöteten Mutanten wird als Mutation Score (Mutationsbewertung) bezeichnet. \n
Ein guter Wert: 80-90% getötete Mutanten. Alles darunter ist ein Grund, die Tests neu zu schreiben.
\n\nPraktisches Beispiel in Java mit PIT
\n\nNehmen wir ein reales Beispiel. Angenommen, wir haben einen Service zur Berechnung von Rabatten:
\n\npublic class DiscountService {\n public double calculateDiscount(double amount, boolean isPremium) {\n double discount = 0.0;\n\n if (amount > 100) {\n discount = 0.1; // 10% Rabatt\n }\n\n if (isPremium) {\n discount += 0.05; // zusätzlich 5%\n }\n\n // Rabatt darf nicht größer als 30% sein\n if (discount > 0.3) {\n discount = 0.3;\n }\n\n return discount;\n }\n}\n\nSchreiben wir einen naiven Test:
\n\nimport org.junit.jupiter.api.Test;\nimport static org.junit.jupiter.api.Assertions.*;\n\nclass DiscountServiceTest {\n\n private final DiscountService service = new