Warum Code Coverage eine Falle ist und Mutation Testing die Rettung
\n\nStellen Sie sich vor: Ihr Kotlin-Projekt ist zu 95% mit Tests abgedeckt. CI ist grün, der Manager ist glücklich, der Code geht in Produktion. Und eine Woche später – ein Bug in Production. Kommt Ihnen das bekannt vor? Höchstwahrscheinlich werden Ihre Tests einfach ausgeführt, aber sie prüfen die Logik nicht. Genau hier kommt Mutation Testing (Mutationstests) ins Spiel – eine Methode, die verhindert, dass Tests "schummeln".
\n\nIn diesem Artikel werden wir uns ansehen, was Mutation Testing in Kotlin ist, wie es funktioniert und wie Sie es mit dem beliebten Tool Pitest in Ihr Projekt integrieren. Sie erfahren, warum 100% Code Coverage keine Garantie für Qualität ist und wie Mutation Testing hilft, wirklich zuverlässige Tests zu schreiben.
\n\nWas ist Mutation Testing? Erklärt anhand von Katzen
\n\nAngenommen, Sie haben eine Funktion geschrieben, die bestimmt, ob eine Katze durch eine Tür passt:
\n\nfun canCatFitThroughDoor(catWidth: Int, doorWidth: Int): Boolean {\n return catWidth <= doorWidth\n}\n\nSie haben einen Test geschrieben:
\n\n@Test\nfun `Katze mit Breite 30 passt durch Tür 40`() {\n assertTrue(canCatFitThroughDoor(30, 40))\n}\n\nDie Abdeckung beträgt 100%. Aber was, wenn jemand versehentlich <= in < ändert? Der Test wird trotzdem bestanden! Mutation Testing führt automatisch solche "kleinen Fehler" (Mutationen) in den Code ein und prüft, ob mindestens ein Test fehlschlägt. Wenn kein Test fehlschlägt, fangen Ihre Tests diesen Fehler nicht. Das ist Mutation Testing in Kotlin.
Wie Mutation Testing funktioniert: Von Operatoren bis zu "überlebenden" Mutanten
\n\nDas Tool (z.B. Pitest für Kotlin) erstellt Mutanten – Versionen Ihres Codes mit einer einzigen Änderung. Hier sind typische Mutationen:
\n\n- \n
- Änderung von Vergleichsoperatoren:
>→>=,==→!=\n - Invertieren von Bedingungen:
if (a > b)→if (a <= b)\n - Entfernen von Methodenaufrufen:
someMethod()→ Leerraum \n - Ersetzen von Rückgabewerten:
true→false\n - Ändern von Zahlen:
10→11oder0\n
Nach der Erstellung der Mutanten werden alle Ihre Tests ausgeführt. Die Ergebnisse fallen in drei Kategorien:
\n\n- \n
- Getötete Mutanten (Killed) – Tests sind fehlgeschlagen, Mutation erkannt. Hervorragend! \n
- Überlebende Mutanten (Survived) – Tests bestanden, obwohl der Code geändert wurde. Ein Alarmzeichen. \n
- Abgedeckt, aber nicht getötet (Covered but not killed) – Tests wurden ausgeführt, haben die Änderung aber nicht bemerkt. \n
Ihr Ziel ist es, so wenige überlebende Mutanten wie möglich zu haben (idealerweise 0). Das ist Mutation Testing von Kotlin-Code in Aktion.
\n\nPitest: Das wichtigste Tool für Kotlin
\n\nFür JVM-Sprachen (Java, Kotlin) ist der De-facto-Standard Pitest (PIT Mutation Testing). Es unterstützt Kotlin über das Plugin pitest-kotlin. So binden Sie es in Gradle ein:
plugins {\n id("info.solidsoft.pitest") version "1.15.0"\n}\n\npitest {\n targetClasses.set(listOf("com.myapp.*")) // welche Klassen mutiert werden sollen\n targetTests.set(listOf("com.myapp.*")) // welche Tests ausgeführt werden sollen\n threads.set(4) // parallele Threads\n outputFormats.set(listOf("XML", "HTML")) // Berichte\n mutationThreshold.set(80) // minimaler Prozentsatz getöteter Mutanten\n timeoutConstInMillis.set(10000) // Timeout pro Test\n}\n\ndependencies {\n testImplementation("org.pitest:pitest-kotlin:1.15.0")\n}\n\nStarten mit einem Befehl:
\n\n./gradlew pitest\n\nNach der Ausführung finden Sie den Bericht in build/reports/pites