Wie gut sind deine Unit Tests? Finde es heraus – und töte Mutanten!

07.07.2020

Eine grüne Test Suite mit hoher Testabdeckung sagt noch nicht unbedingt viel über die Qualität der Tests aus. Die wichtigste Frage beantwortet sie nicht: decken die Tests potenzielle Bugs auf, die z.B. bei einem Refactoring entstehen? Die Antwort auf diese Frage liefert das Mutation Testing.

Wie gut kann ich mich auf meine Test Suite verlassen?

Üblicherweise verlässt man sich beim Refactoring auf die Unit & Integration Tests – wahrscheinlich umso mehr, je höher die Coverage ist. Genau dafür sind die Tests ja da. Dennoch ist eine hohe Testabdeckung allein noch kein Garant dafür, dass die Tests alle ungewünschte Änderungen am Verhalten der Software aufdecken. Im Extremfall könnten z.B. in allen Tests die assert-Statements fehlen. Das Resultat: trotz hoher Testabdeckung wird im Grunde nichts getestet. Nun, zum Glück ist das nicht der Normalfall ;-). Dennoch passieren Fehler, daher kann es schon mal vorkommen, dass das assert– oder verify-Statement in einem Test versehentlich weggelassen wird. Zudem ist es durchaus möglich, dass Testfälle fehlen, obwohl der Anwendungscode zu 100% von den Tests ausgeführt wird.

Mit Mutation Testing zur sicheren Testsuite

Beim Auffinden solcher Mängel oder Lücken in den Tests kann Mutation Testing behilflich sein. Für Java gibt es zu diesem Zweck das Framework PIT bzw. Pitest. Und wie funktioniert das Ganze?

Ausgehend von einer grünen Test Suite verändert das Framework den Anwendungscode. Dazu stehen ihm eine ganze Reihe von Mutatoren zur Verfügung. Einer davon negiert beispielsweise die Bedingungen von if-Statements.

Anwendungscode:

if (a == b) { ... }

Mutierter Code:

if (a != b) { ... }

Nun wird die Testsuite gegen die so mutierte Anwendung ausgeführt. Führt die Mutation zum Fehlschlagen eines oder mehrerer Tests, wurde der Mutant „getötet“. Das hießt, unser Code ist bereits gegen diese Änderung abgesichert. Bleiben jedoch alle Tests grün, hat der „Mutant“ überlebt – und somit eine potenzielle Lücke in der Testsuite aufgedeckt.

Entsprechend werden etliche weitere Mutatoren angewendet und getestet, z.B.:

  • Änderung von Wertebereichen: aus if (a > b) wird if (a >= b)
  • Negieren der Rückgabewerte von Methoden: aus return i; wird return -i;
  • Methoden geben null zurück: aus return myObj; wird return null;
  • Aufrufe von void-Methoden werden entfernt
  • etc.

Am Ende erhält man einen Mutation Coverage Report, der wie folgt aussieht:

Wie man sehen kann, hat die Testsuite des hier geprüften Projekts eine zeilenbasierte Testabdeckung von 100%; die Mutation Coverage liegt jedoch nur bei 83%. Die Klasse SystemTimeProvider hat trotz einer Testabdeckung von 100% eine Mutation Coverage von 0%! Wollen wir doch mal herausfinden wieso, und klicken auf den Klassennamen:

Der Report verrät uns, dass der Rückgabewert von getCurrentTime() durch null ersetzt worden ist. Gemäß der Line-Coverage wurde die Methode währen der Testausführung aufgerufen. Allerdings wird der Rückgabewert in den Tests wohl nie verwendet oder geprüft. Dass getCurrentTime() plötzlich null zurückliefert, wurde von keinem der Testfälle entdeckt.

Weiter unten im Report sieht man, welche Mutatoren aktiv und im Testlauf potenziell zum Einsatz gekommen sind – passenden Code vorausgesetzt.

Schaut man sich nun alle Mutanten an, die überlebt haben, bekommt man Anregungen für Testfälle, mit denen die existierende Testsuite ergänzt werden kann.

Verwendung

Die Verwendung von Pitest ist ganz einfach. In einem Maven-Projekt beispielsweise muss man noch nicht einmal etwas an der Projektkonfiguration ändern. Es genügt, wenn man auf der Kommandozeile im Projektverzeichnis den folgenden Befehl ausführt:

$ mvn org.pitest:pitest-maven:mutationCoverage

Der Report befindet sich anschließend im Verzeichnis target/pit-report/YYYYMMDDHHMI (Ordner benannt nach aktuellem Datum & Uhrzeit).

Pitest kann über die pom.xml in den Buildprozess eingebunden werden. Die Frage ist allerdings, ob man das möchte. Obwohl Pitest schon um einiges schneller ist als andere Mutation Testing Frameworks, kann die Ausführung je nach Umfang des Projekts und der Testsuite recht lange dauern, was in Bezug auf schnelles Feedback eher hinderlich ist. Wann aber ist die Erstellung von Mutation Coverage Reports sinnvoll?

Ein guter Zeitpunkt ist zum Beispiel vor einem Refactoring. Hier kann der Mutation Coverage Report genutzt werden, um Lücken im „Sicherheitsnetz Unit Tests“ zu schließen. Zudem gibt es neben mutationCoverage das Goal scmMutationCoverage. Damit werden nur Klassen geprüft, die im Sourcecode-Verwaltungssystem den Status „geändert“ oder „hinzugefügt“ haben. Dies geht relativ schnell, und man kann vor dem Einchecken prüfen, ob den Klassen, an denen man aktuell entwickelt, noch Tests fehlen.

Wer Pitest erst einmal unabhängig von eigenen Projekten testen möchte, kann auch das Mutation Testing Kata ausprobieren, das auf Github verfügbar ist.

 

Fazit

Mutation Testing prüft die Qualität der Testsuite, indem es Fehler im Anwendungscode produziert und anschließend die Tests darauf loslässt. Bleibt die Testsuite grün, ist ein potenzieller Fehler nicht durch Testfälle abgedeckt. Das Mutation Testing Framework Pitest ist einfach zu verwenden und kann dabei helfen, die eigene Testsuite zu einem Sicherheitsnetz zu machen, auf das man sich auch wirklich verlassen kann.

 

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*