Mutations-Testing mit PIT (PITest) ist eine fortgeschrittene Methode zur Bewertung der Qualität von Testsuiten. PIT modifiziert den Quellcode automatisch durch kleine Änderungen (Mutationen) und prüft, ob die bestehenden Tests diese Abweichungen zuverlässig erkennen. Nicht getötete Mutanten weisen auf Lücken in der Testsuite hin.
PIT (PITest) beantwortet eine andere Frage als JaCoCo. JaCoCo misst, ob Ihr Code von den Tests
durchlaufen wird. PIT misst, ob Ihre Tests einen Fehler überhaupt bemerken würden.
Dazu verändert PIT Ihren kompilierten Code automatisch an vielen Stellen, jeweils minimal, und
erzeugt so Mutanten. Beispiele: aus >= wird >, aus + wird -, aus * 2 wird / 2, eine
if-Bedingung wird negiert, ein Rückgabewert wird durch 0 ersetzt. Für jeden Mutanten lässt PIT
Ihre Testsuite erneut laufen:
Der Anteil getöteter Mutanten (Mutation Coverage) misst damit die Wirksamkeit Ihrer Tests, nicht nur deren Reichweite.
Wie Sie PIT lokal ausführen und den Report finden, beschreibt die Infopage JaCoCo und PIT lokal ausführen.
Hohe Coverage sagt wenig über die Qualität der Tests. In diesem Projekt zeigt das die bewusst
schwache Testsuite für LoyaltyPoints (siehe unten): Sie erreicht rund 91 % Zeilenabdeckung, aber
nur 38 % Mutation Coverage, von 13 Mutanten überleben 8. Coverage färbt fast alles grün, PIT deckt
auf, dass die Tests kaum etwas wirklich prüfen.
Die Übersicht listet pro Package und Klasse drei Zahlen:

Ein Klick auf eine Klasse zeigt den Quelltext. Jede Zeile, an der PIT mutiert hat, ist markiert und unter dem Code nummeriert aufgelistet, mit Status und angewandtem Mutator:

PIT verwendet hier die Standard-Mutatoren, darunter:
< zu <=, > zu >=).+ - * / %).true/false/0/null).Wichtig ist eine Eigenheit: PIT mutiert nur primitive Arithmetik. Rechnen Sie mit BigDecimal oder
verstecken Sie die Logik hinter Methodenaufrufen, findet PIT kaum Angriffsfläche. Genau deshalb ist
das Beispiel hier bewusst mit einfachen int-Operationen geschrieben.
LoyaltyPointsLoyaltyPoints.forTicket(...) berechnet Bonuspunkte für ein Bahnticket mit einfachen
int-Operationen: Punkte pro Euro, Bonus für lange Strecken, Verdopplung für Vielfahrer, eine
Obergrenze.
public int forTicket( int fareEuros, int distanceKm, boolean frequentTraveller ) {
if ( fareEuros < 0 || distanceKm < 0 ) {
throw new IllegalArgumentException( "fare and distance must not be negative" );
}
int points = fareEuros * POINTS_PER_EURO;
if ( distanceKm >= LONG_DISTANCE_KM ) {
points += LONG_DISTANCE_BONUS;
}
if ( frequentTraveller ) {
points *= FREQUENT_TRAVELLER_FACTOR;
}
if ( points > MAX_POINTS_PER_TICKET ) {
points = MAX_POINTS_PER_TICKET;
}
return points;
}
Die mitgelieferte Testsuite ist absichtlich schwach: Sie ruft die Methode in jedem Zweig auf (daher die hohe Zeilenabdeckung), prüft aber nur grobe Eigenschaften, nie einen konkreten Wert:
> 0),>=),<=).class LoyaltyPointsTest {
private final LoyaltyPoints loyaltyPoints = new LoyaltyPoints();
@Test
void aTicketEarnsPoints() {
assertTrue( loyaltyPoints.forTicket( 30, 50, false ) > 0 );
}
@Test
void aLongTripEarnsAtLeastAsMuchAsAShortTrip() {
assertTrue( loyaltyPoints.forTicket( 30, 100, false ) >= loyaltyPoints.forTicket( 30, 50, false ) );
}
@Test
void aFrequentTravellerEarnsAtLeastAsMuch() {
assertTrue( loyaltyPoints.forTicket( 30, 50, true ) >= loyaltyPoints.forTicket( 30, 50, false ) );
}
@Test
void pointsNeverExceedTheCap() {
assertTrue( loyaltyPoints.forTicket( 100000, 100000, true ) <= 1000 );
}
}
Im PIT-Report zu LoyaltyPoints.java (siehe oben) sehen Sie, was diese Lücken kosten. Einige der überlebenden
Mutanten und warum sie durchrutschen:
fareEuros * POINTS_PER_EURO wird zu fareEuros / POINTS_PER_EURO. Der Test prüft nur > 0,
auch eine Division ergibt etwas Positives, also bleibt er grün.distanceKm >= 100 wird zu > 100. Bei genau 100 km entfällt dann der Bonus. Da der
Test nur lang >= kurz vergleicht (und ohne Bonus eben 60 >= 60 gilt), merkt er nichts.if (frequentTraveller) wird negiert, die Verdopplung entfällt also. Der
>=-Vergleich “Vielfahrer bekommt mindestens so viel” ist auch dann noch wahr.points > MAX wird zu >=. Ohne einen Test, der genau an der Grenze prüft,
fällt der Unterschied nicht auf.Jeder überlebende Mutant zeigt also präzise auf eine fehlende Prüfung.
Die Behebung ist immer dieselbe Idee: Statt grober Ungleichungen konkrete Werte behaupten (z. B. genau 110 Punkte für eine bestimmte Fahrt) und gezielt die Grenzen abklopfen (genau 100 km, genau an der Obergrenze, und auch eine negative Eingabe). Schreiben Sie pro überlebendem Mutanten den Test, der ihn töten würde, und lassen Sie PIT erneut laufen: Die roten Stellen werden grün, und die Mutation Coverage steigt. Den konkreten Testcode dazu schreiben Sie selbst, das ist die eigentliche Übung.