
Warum Code Coverage nicht reicht
Code Coverage kann trügerisch sein, wenn es um die Qualität von Unit-Tests geht. Die meisten Projekte messen Code Coverage über das reine Testen hinaus und streben oft eine Abdeckung von über 70% an, manche sogar eine nahezu vollständige Abdeckung.
Ein einfaches Beispiel
Eine einfache Additionsfunktion:
public abstract class Numbers {
static int add(int a, int b) {
return a + b;
}
}
Mit dem dazugehörigen Test:
public class NumbersTest {
@Test
void resultIsSumOfBoth() {
assertThat(Numbers.add(2,2)).isEqualTo(4);
}
}
Tests können mit hoher Coverage bestehen und trotzdem Mutationen nicht erkennen – zum Beispiel wenn a + b zu a * b
geändert wird. Der Test würde immer noch bestehen, da 2 + 2 und 2 * 2 beide 4 ergeben.
Alternative Teststrategien
Es gibt zwei Ansätze, um dieses Problem zu lösen:
- Property-based Testing – statt Konstanten generierte Werte verwenden (z.B. mit jqwik )
- Mutationstests – absichtlich Fehler in den Bytecode einbauen und prüfen, ob die Tests diese erkennen
PI Test ist ein Plugin, das diese Art von Analyse ermöglicht.
Mutationstests mit PI Test
Die Verbreitung von PI Test ist noch begrenzt – hauptsächlich wegen der Ausführungszeit (es sind mehrere Testdurchläufe erforderlich) und der Menge an Problemen, die in bestehenden Projekten aufgedeckt werden. Dennoch ist das Tool wertvoll für kritischen Code, der hohe Zuverlässigkeit erfordert.
PI Test in der Anwendung
Die Konfiguration als Maven-Plugin:
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.7.2</version>
<executions>
<execution>
<goals>
<goal>mutationCoverage</goal>
</goals>
</execution>
</executions>
<configuration>
<mutators>
STRONGER,AOR,AOD,OBBN,ROR,REMOVE_INCREMENTS,NON_VOID_METHOD_CALLS,INLINE_CONSTS,CONSTRUCTOR_CALLS
</mutators>
<timestampedReports>false</timestampedReports>
<verbose>false</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>0.15</version>
</dependency>
</dependencies>
</plugin>
Bessere Tests
Eine Funktion zur Bereichsprüfung:
public abstract class Numbers {
static boolean isInRange(int value, int min, int max) {
if (value<min) return false;
if (value>max) return false;
return true;
}
}
Verbesserte Tests mit Grenzfällen:
@Test
void valueIsBetween() {
assertThat(Numbers.isInRange(3, 3, 4)).isTrue();
assertThat(Numbers.isInRange(4, 3, 4)).isTrue();
}
Der entscheidende Unterschied zwischen den Tests in diesem Beispiel ist einfach: Wir haben die Grenzfälle getestet.
Zusammenfassung
- Unit-Tests stellen sicher, dass das erwartete Verhalten unter definierten Bedingungen gegeben ist
- Code Coverage identifiziert nicht getesteten Code
- Mutationstests helfen, bessere Tests zu schreiben, die unerkannte Fehler verhindern
