Logo von Developer

Suche
preisvergleich_weiss

Recherche in 2.383.371 Produkten

Johannes Dienst 6

Mutationstests als Abhilfe

Unit-Tests gehören bei vielen Entwicklern zur täglichen Arbeit als Schutz vor Regressionen und als lebende Dokumentation. Aber wie lässt sich die Qualität der Tests überprüfen?

Paarprogrammierung, testgetriebene Entwicklung und Code Reviews können zwar helfen, die blinden Flecke zu beseitigen, sie sind aber nicht automatisiert und daher nicht erschöpfend ausgelegt. Es bleibt ein gewisser Prozentsatz von unzureichenden beziehungsweise fehlenden Tests bestehen.

An dieser Stelle schaffen Mutationstests Abhilfe. Bereits 1971 von Richard Lipton vorgeschlagen, erlebt es in den letzten Jahren eine Renaissance in diversen Programmiersprachen. Dabei hilft die immer größere Rechenleistung von Mehrkernprozessoren, mit der sich diese Art der Analyse praktikabel umsetzen lässt. Zeitgemäße Werkzeuge nutzen die Parallelisierung, um den Durchsatz erheblich zu steigern.

Mutationstests basieren auf zwei Annahmen: Die erste ist, dass Programmierer kompetent sind und keine groben Fehler machen, die zu offensichtlichen Bugs führen. Da die Disziplin des Programmierens anspruchsvoll ist und viel Code entsteht, ist es aber wahrscheinlich, dass kleine Ungenauigkeiten entstehen, die zunächst nicht auffallen. Die zweite Annahme ist der sogenannte Kopplungseffekt. Gepaart mit der ersten Annahme, dass Fehler unvermeidlich entstehen, führt die Anhäufung kleiner Fehler schließlich doch zu einem Systemfehler.

Mutationstests machen sich diese Annahmen zunutze, indem es sogenannte Mutanten erzeugt, die jeweils genau eine kleine Änderung enthalten: die Ungenauigkeit beziehungsweise Regression. In Abbildung 1 sind die Mutanten mit M1, M2 und so fort ausgezeichnet. Anschließend erfolgt die Ausführung der Testsuite auf jedem Mutanten. Schlägt mindestens ein Test fehl, ist die Suite in der Lage, die künstlich eingebaute Regression zu finden. Im Jargon des Mutationstestens spricht man vom Töten der Mutante. Ansonsten überlebt der Mutant und damit die Regression. Je mehr Mutanten getötet werden, desto sicherer schützt die Testsuite vor Regressionen.

Vorgehensweise beim Testen von Mutationen (Abb. 1)
Vorgehensweise beim Testen von Mutationen (Abb. 1)

Mutationstests stellen sicher, dass die Tests bei der Überdeckung einer Codezeile in der Lage sind, Abweichungen zu entdecken. Es überprüft also, ob der Test für eine überdeckte Codezeile sinnvoll verläuft. Der Autor von PIT, einem Tool für Java, bezeichnet Mutationstests daher als Goldstandard.

Art der Mutatoren

Auch wenn es eine große Zahl unterschiedlicher Mutatoren gibt, beschränkt sich dieser Artikel auf einige Standard-Mutatoren von PIT, die als gutes Beispiel dienen. Weitere Informationen finden sich in der Übersicht bei PIT.

Die im Folgenden vorgestellten Mutatoren mögen trivial erscheinen. Unter Berücksichtigung der grundlegenden Annahmen von Richard Lipton sind sie jedoch sinnvoll. Gerade einfache Fehler und Unachtsamkeiten, die nicht auffallen, führen im Alltag zu unerwünschtem Systemverhalten.

Bedingungen wie <, <=, > und >= sind das Ziel des Bedingungsgrenzen-Mutator. Er ändert die Grenzen wie in folgender Tabelle gezeigt:

Original Mutation
< <=
<= <
> >=
>= >

Aus dem Code

if (a < b) { }

wird im mutierten Zustand

if (a <= b) { }

Der mathematische Mutator ist sehr umfangreich. Er sucht sich alle mathematischen Ausdrücke und mutiert sie systematisch zu ihrem jeweiligen Gegenteil. Beispielsweise wird aus einer Multiplikation eine Division und umgekehrt. Das kann folgendermaßen aussehen:

int x = y * z;

wird zu

int x = x / z;

Der Rückgabewert-Mutator verändert die Rückgabewerte in das Gegenteil und zwar auf eine sichere Weise, sodass bei float und double kein NaN (Not a Number) zurückgegeben wird.

Rückgabetyp Mutation
boolean true wird zu false und false wird zu true
int, byte, short 0 wird zu 1, alles Andere zu 0
long x wird um 1 erhöht zu x+1
float, double X wird zu -(x+1.0), wenn x eine Zahl ist, NAN wird durch 0 ersetzt
Object Nicht null wird durch null ersetzt. Falls die unmutierte Methode null zurückgibt, wird eine java.lang.RuntimeException geworfen

Die nicht mutierte Version von

public boolean isEmpty() {
return this.myList.size == 0;
}

wird zu

public boolean isEmpty() {
return (this.myList.size == 0) ? false : true ;
}

Der letzte Mutator ist nützlich, um Nebeneffekte zu finden, die durch Tests abgedeckt sind, deren Veränderung die Tests aber nicht bemerken. Dafür entfernt der void-Methoden-Mutator die Aufrufe zu Methoden mit dem Modifizierer void. In ihnen verstecken sich oft Seiteneffekte wie das Speichern von Ergebnissen in eine Datenbank, die bei Tests gerne vergessen werden.

Bei folgendem Codeabschnitt:

public void saveToDatabase(Object o) { /* Save it */ }
public Object updateObject(Object o) {
o.setState("active");
saveToDatabase(o);
}

fällt in der mutierten Version der Nebeneffekt weg

public Object updateObject(Object o) {
o.setState("active");
}

Um Seiteneffekte zu testen, eignet sich ein Spy, ein Mock-Objekt, das die Aufrufe protokolliert. Mit ihm lässt sich überprüfen, ob eine Methode tatsächlich aufgerufen wurde oder auf irgendeine Art und Weise weggefallen ist – wie im obigen Beispiel. Zwar ist das Schreiben von Tests, die auf diese Weise arbeiten, nicht schwierig, aber im Alltagsgeschäft fällt ein Spy häufig unter den Tisch, da er mehr Aufwand verursacht und auf den ersten Blick nicht notwendig erscheint.

6 Kommentare

Themen: