Mutation Testing | Teil 2

Im letzten Blogpost Mutation Testing Teil 1 haben wir die Grundlagen des Mutation Testing kennengelernt und erfahren, wie wir mit Hilfe des Mutation Reports unsere Tests verbessern können. Im zweiten Teil der Blogpost-Serie wollen wir nun das Mutation Testing Framework PIT  genauer anschauen und zeigen, wie man dieses in seinen eigenen Code integrieren kann.

Icon eines Dokuments mit Testresultaten

 

PIT ist eines der meist gebrauchten Mutation Testing Frameworks. Es ist Open Source und wird aktuell gepflegt und weiterentwickelt. Folgendes ist generell zu PIT interessant:

  • Zur Nutzung von PIT wird die Installation von Java 5+ und JUnit 4.6+ (oder TestNG 6) vorausgesetzt.
  • Zur Ausführung der Mutation Tests kann Maven, Gradle oder ein Plugin für das integrated development environment (IDE) verwendet werden.
  • Die Erstellung der Mutanten Px aus dem Original Programm P (indem jeweils eine einzelne Mutation auf den Code angewendet wird) wird durch Bytecode Manipulation (ASM Library) erstellt. So ist die Erstellung der Mutanten schneller, als wenn jeder Mutant aus verändertem Source Code (mit anschliessender Compilierung) erstellt werden würde.
  • Bei Performance Probleme können Tests parallel und isoliert voneinander ausgeführt werden.

Mutators

PIT bietet eine grosse Anzahl möglicher Mutationen (auch Mutators genannt). Diese werden in Gruppen eingeteilt und können konfiguriert werden. Aktuell gibt es folgende Gruppen:

  • OLD_DEFAULTS
  • DEFAULTS
  • STRONGER
  • ALL

Die Anzahl der Gruppen und welche Mutators in der Gruppe sind, kann sich von Release zu Release ändern. Mehr Informationen dazu findest du auf https://pitest.org/quickstart/mutators.

Via pom.xml kann konfiguriert werden, welche Mutator Gruppen verwendet werden sollen. Ohne spezielle Konfiguration kommen automatisch die Mutators von DEFAULTS zum Einsatz. PIT entscheidet dann aber selbst (aufgrund des zu testenden Codes), welche Mutators einer Gruppe konkret verwendet werden. Im Report ist unter “Active mutators” ersichtlich, welche Mutators zu Verfügung stehen.

Im folgenden Bild zeigen wir als Beispiel eine Konfiguration, in der zwei Gruppen genutzt werden: DEFAULTS und OLD_DEFAULTS.

In der DEFAULTS Gruppe stehen diverse Mutators zur Auswahl:

Conditionals Boundary Mutator (CONDITIONALS_BOUNDARY)
Ein “<“ wird zu einem “<=” oder ein “<=” wird zu einem “<”.

Negate Conditionals Mutator (NEGATE_CONDITIONALS)
Ein “==” wird zu einem “!=” oder ein “<” zu einem “>=”.

Math Mutator (MATH)
Mathematische Operationen werden ersetzt. z.B. + durch – oder * durch /.

Increments Mutator (INCREMENTS)
Ein ++ wird durch – – ersetzt und umgekehrt.

Invert Negatives Mutator (INVERT_NEGS)
Bei einem Integer oder Float/Double wird das Vorzeichen gewechselt. So wird ein -a zu einem a.

Void Method Call Mutator (VOID_METHOD_CALLS)
Wenn eine Methode eine andere aufruft, die nur void zurückgibt, dann wird dieser Aufruf nicht mehr gemacht.

Return Values Mutator (RETURN_VALS)
Die Return Werte werden verändert: true -> false, bei int Werten wird 0 oder 1 zurückgegeben, bei Objekten null oder eine RuntimeException geworfen.

Empfohlen wird der Start mit der DEFAULTS Gruppe. Je mehr Mutators eine Gruppe hat, desto schwieriger wird es, einen guten Mutation Score zu erreichen. Jedoch kann experimentiert und selbst entschieden werden, wann genau gestoppt werden soll.

Setup mit Maven und JUnit

Nun betrachten wir ein minimales Setup für Maven und JUnit 5. Information für andere Setups gibt es direkt auf der PIT Webseite.

Folgende Punkte sind wichtig:

  • Das pitest Plugin: pitest-maven
  • Ein Plugin, um mit JUnti 5 arbeiten zu können: pitest-junit5-plugin
  • Unter configuration kann das Default-Verhalten angepasst werden. In unserem Fall haben wir nur einen Filter definiert, um zu selektionieren, welche Klassen PIT anschauen soll.

Nun kann es schon losgehen: Wir builden ein Projekt wie gewohnt und starten PIT:

mvn clean install
mvn org.pitest:pitest-maven:mutationCoverage

Unter “target > pit-reports” wird ein Verzeichnis erstellt. Der Name besteht aus Zahlen und enthält den Zeitstempel, wann der Report generiert worden ist. Hier steht 202204091301 für 2022-04-09 13:01 (9. April 2022 um 13:01). Pro Test Run wird jeweils ein neuer, zusätzlicher Ordner erstellt. In index.html ist der eigentliche Report. 

Beispiel

Zur Veranschaulichung machen wir ein Beispiel. Wir nehmen den Code des CalculatorService aus dem Blogpost Teil 1 und erweitern ihn um die Mehrwertsteuer. Für einen Input total grösser 200 gibt es 50 Rabatt. Zusätzlich wird noch die Mehrwertsteuer berücksichtigt.

Mit dem gleichen JUnit Test aus der letzten Folge erhalten wir eine Code Coverage von 100%.

Nun wechseln wir in die Mutation Testing Welt und generieren einen Report mit:

mvn clean install
mvn org.pitest:pitest-maven:mutationCoverage

  • Wir öffnen den Report und sehen:
  • Line Coverage 100%
  • Mutation Coverage nur 50%

Die Coverage wird zusätzlich pro Package angegeben. Durch einen Click auf den Package-Namen (hier ch.jduke) erhalten wir Details zur Coverage im Package und den einzelnen Klassen.

Wir sehen:
Alle Zeilen haben einen grünen Hintergrund, d.h. die Code Coverage ist 100%. Es gibt 6 Mutationen, und zwar in den Zeilen 7, 8 und 10:

  • zwei Mutationen in Zeile 7
  • eine Mutation in Zeile 8
  • drei Mutationen in Zeile 10 (jeweils ersichtlich durch die Ziffern 1,2,3 in der 2. Spalte)

Dies schauen wir uns nun im Detail an:

Zeile 7: es gibt zwei Mutationen; NEGATE_CONDITIONALS und CONDITIONALS_BOUNDARY

  • Bei NEGATE_CONDITIONALS wird aus “> 200” ein “<= 200” gemacht. Ein JUnit Test ist rot geworden und daher hat der Mutant Status KILLED. Für diesen Fall sind unsere Tests gut.
  • Bei CONDITIONALS_BOUNDARY wird aus “> 200” ein “>= 200”. Leider hat dies keiner unserer Tests bemerkt und der Mutant hat Status SURVIVED. Wenn wir die Tests genauer betrachten, sehen wir, dass wir auf einen Wert ‚kleiner 200‘ und einen Wert ‚grösser 200‘ testen, aber nicht genau auf den Wert 200. Exakt dies hat der Mutatant mit dem Wechsel auf “>= 200” gemacht. Wir passen den Test an, ändern unseren Test-Parameter von 199 auf 200 und lassen PIT nochmals laufen. Nun erhalten wir einen Mutation Score von 67% und Zeile 7 ist nun grün.


Zeile 8: es gibt eine Mutation INVERT_NEGS.
Bei INVERT_NEGS wird aus 50 neu -50, was von unseren Tests bemerkt wurde. Daher hat der Mutant Status KILLED.

Zeile 10: es gibt drei Mutationen: (2x) MATH und NULL_RETURNS

  • Bei NULL_RETURNS wird als Resultat statt dem berechneten Preis einfach 0 zurückgegeben. Dies bemerken allerdings unsere Tests. Der Mutant hat Status KILLED.
  • Bei MATH wird in der Preisberechnung + durch – und * durch / ersetzt. Da unsere Tests als Parameter für tax 0 übergeben, merken unsere Tests diese Mutation nicht. Die beiden Mutanten haben Status SURVIVED. Also passen wir unsere Tests erneut an.

Nach den beiden Anpassungen sehen die Tests nun folgendermassen aus:

 Der gelieferte Mutation Score beträgt 100%.

Fazit

In diesem zweiten Teil der Blogpost-Serie haben wir PIT näher betrachtet und uns einen Überblick über die verschiedenen unterstützten Mutationen verschafft. Dabei haben wir festgestellt, wie leicht man PIT in ein Java (Maven) Projekt mit bestehenden JUnit 5 Tests integrieren kann. Mit Hilfe eines kleinen Beispiels und dem generierten Report gelang es uns, unsere Tests zu verbessern. Eine erfreuliche Erkenntnis :-).

Weiterführende Links:

PIT – https://pitest.org
Getting started – https://pitest.org/quickstart/
Überblick Mutators – https://pitest.org/quickstart/mutators/

Kommentare sind geschlossen.