Mutation Testing oder wie gut sind meine Tests wirklich?

Alles Grün oder was?

Zufrieden blicke ich auf die vielen grünen Symbole auf meinem Bildschirm. Alle meine Unit-Tests bestätigen mir, meine Software funktioniert genau wie erhofft. Zudem bescheinigt mir ein Werkzeug, dass meine Testabdeckung so hoch ist, dass (fast) alle Zeilen bei jedem Durchlauf meiner Unit-Test-Suite getestet werden. Und das Ganze automatisiert, versteht sich.

Neulich gab es trotz aller Vorsichtsmaßnahmen dann dennoch einen Bug im Produktiv-Code. Sicherlich nichts Wildes, jedoch ausreichend, um sich zu fragen, wie der Fehler an den Tests und dem Code-Review vorbei kommen konnte.

Tests anhand von Mutation Testing bewerten
“A Scale” by Bryce Edwards is licensed under CC BY 2.0

Das Projekt ist jedoch nun schon einige Zeit alt und die Tests an sich sind auch ziemlich „gewachsen“. Sie sind inzwischen recht umfangreich und umfassen viele Zeilen Code. So richtig vermag ich es nicht einzuschätzen, wie gut die Tests eigentlich sind, wenn ich ehrlich bin, aber ich kann ja wohl keine Tests für die Tests schreiben!?! Mal abgesehen davon, dass die Idee ziemlich absurd ist, wie soll ich das denn dem Projektleiter und Budgetverantwortlichen erklären? Tests für die Tests… also wirklich. Wie lassen sich dennoch Aussagen zur Qualität meiner Tests treffen?

Dieses Problem muss doch auch schon ein anderer gehabt haben, frage ich mich. Und tatsächlich, wie zu vielem im Feld der Informatik, haben sich über solche Fragestellungen Leute bereits in der Vergangenheit Gedanken gemacht. Schon Ende der 70er wurde die Praktik des „Mutation Testings“ beschrieben. [1]

Die Mutanten sind los

Die Idee ist einfach. Alle Tests werden ausgeführt und, vorausgesetzt sie laufen grün, werden Mutanten erzeugt, das heißt es werden gezielt einzelne Änderungen am Produktiv-Code vorgenommen. Aus einem:

if (a < b) {
  // ...
}

wird beispielsweise ein:

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

Anschließend werden die Tests erneut ausgeführt und überprüft, ob sie diese Veränderung bemerken. Fällt es auf, spricht man davon, dass der Mutant getötet wurde. Bleibt die Veränderung unbemerkt, heißt es, dass der Mutant überlebt hat.

Damit ein Mutant getötet werden kann, müssen drei Bedingungen erfüllt sein:

  1. Ein Test muss die mutierte Code-Stelle erreicht haben.
  2. Es muss einen Test geben, der das mutierte Programm dazu veranlasst in einen anderen Zustand zu geraten. Im obigen Beispiel müsste ein Test mit a = b existieren.
  3. Die Veränderung muss zu einer Änderung des Programmverhaltens führen und dies muss von einem Test bemerkt werden.

Abhängig vom eingesetzten Framework und der Technologie werden diverse Mutanten erzeugt. Hier ein kurzer, beispielhafter Ausschnitt:

Original Mutant
!= ==
== !=
a==b true
+
/ *
return new Object() return null

 

Für jede Veränderung wird die komplette Test-Suite ein weiteres Mal durchlaufen. Nach Abschluss des Mutierens und Testens wird ein Bericht über alle getöteten und verbliebenen Mutanten erzeugt. Ein hoher Prozentsatz an getöteten Mutanten spricht für eine hohe Qualität an Tests.

Überleben Mutanten, wird mir genau angezeigt, welche Veränderung zu dem überlebenden Mutanten geführt hat.

Auswertung

Code-Schnipsel
Screenshot @ Benjamin Klüglein

 

In obigem Ausschnitt aus einem Beispielbericht ist zu erkennen, dass in Zeile 5 ein Mutant überlebt hat. Die Ausgabe des Tests ist wie folgt zu interpretieren: An dieser Stelle wurden zwei Mutanten erzeugt. Einmal wurde die Bedingung negiert, was jedoch zu einem Mutanten geführt hat, der getötet wurde. Für den zweiten Mutanten wurde aus dem >= ein >. Dieser Mutant wurde nicht durch meine Tests getötet, weil ein Test-Case für den Fall price = 50 nicht existiert. An dieser Stelle wurde also Verbesserungspotenzial in meinen Tests entdeckt.

Die meisten Mutation Testing Frameworks bieten eine große Menge an verschiedenen Mutatoren (siehe hier die Liste der Mutatoren des PIT-Frameworks), also Veränderungen, am Code an. Zudem erlauben sie die Konfiguration, welche Mutatoren zum Einsatz kommen.

 

Einsatz von Mutation Testing

Der Einsatz des Mutation Testings gibt mir einen praktischen Ansatz, die Qualität von Tests zu bewerten und zu schauen, wo die Tests noch verbesserungswürdig sind.

Da der Vorgang, abhängig vom Umfang der Test-Suite, recht lange dauern kann, empfiehlt es sich, die Mutation Tests auf einem CI-System – wie etwa Jenkins – laufen zu lassen.

Unter gewissen Umständen kann es vorkommen, dass false positives auftreten, das heißt Mutanten, die nicht getötet werden können. Daher ist ein Überlebender mitunter kein Zeichen eines schlechten Tests.

Werkzeuge

Es gibt diverse Frameworks für das Mutation Testing für alle gängigen Programmiersprachen. Meine Beispiele sind mit PIT und Java umgesetzt. Eine Auswahl an Frameworks findet sich im Wikipedia-Artikel zu Mutation Testing.

Um die Nutzung zu erleichtern, gibt es diverse Plugins für die Integration in IDEs, zum Beispiel für Eclipse oder Build-Tools, wie Maven und Ant.

 

Einstieg

Für den Einstieg empfehle ich, sich zunächst ein Framework passend zur eingesetzten Technologie zu suchen. Dort findet sich dann meist eine Hilfestellung, wie das Framework in die eigenen Werkzeuge zu integrieren ist.

Technologie Framework
Java PIT
JavaScript Stryker
C#/IL VisualMutator

Ein kleines Java-Beispiel findet sich auch unter https://github.com/methodpark/mutation-testing.

 

In diesem Sinne wünsche ich frohe Mutanten-Suche.

 

[1] Richard A. DeMillo, Richard J. Lipton, and Fred G. Sayward. Hints on test data selection: Help for the practicing programmer. IEEE Computer, 11(4):34-41. April 1978

 

Ähnliche Artikel