Code Coverage ist tot. Lang lebe Mutation Testing!

Die wahrscheinlich mühsamste Arbeit beim Entwickeln eines Software-Produktes ist manuelles Testen. Das Durchklicken des Produkts ist nicht nur zeitaufwendig, sondern es ist auch schwierig, alle Spezialfälle beim Testen abzudecken. Automatisierte Tests sollen hier Abhilfe schaffen und werden heutzutage flächendeckend eingesetzt.



Wie viele Tests brauche ich?

Für Entwickler ist es jedoch schwer einzuschätzen, wie viele Tests notwendig sind und welche Spezialfälle von den Tests abgedeckt werden müssen, um die Qualität der Software sicherzustellen. Eine weit verbreitete Metrik ist die Testabdeckung oder Code Coverage, mit der festgestellt werden kann, welche Teile des Codes von den Tests durchlaufen werden.

Das Problem bei der Code-Coverage-Metrik ist jedoch, dass eine hundertprozentige Abdeckung schwer erreichbar und nicht immer sinnvoll ist. Außerdem lässt sich aus einer hohen Abdeckung nur bedingt auch auf eine hohe Qualität der Tests schließen. Zum Beispiel kann ein Entwickler die Code Coverage sehr einfach erhöhen, indem er einfach noch ungetestete Funktionen in den Tests aufruft, aber keine Spezialfälle testet.

Wie gut sind meine Tests?

Eine immer häufiger eingesetzte Technik, die Qualität von Tests zu bestimmen, ist Mutation Testing. Dabei werden einzelne kleine Fehler (Mutanten) in den Code eingeschleust. Für jeden solchen Mutanten wird die ganze Test-Suite ausgeführt. Laufen alle Tests erfolgreich durch, wird der Fehler nicht erkannt und der Mutant überlebt. Die resultierende Metrik ist die sogenannte Kill-Rate der Mutanten (also wie viele Mutanten/eingeschleuste Fehler erkannt wurden).

Beispiele solcher Mutanten sind zum Beispiel:

  • Adaptieren einer Condition (z.B. Ändern von < auf <=)
  • Adaptieren einer mathematischen Operation (z.B. Ändern von + auf -)
  • Löschen eines Methoden-Aufrufs
  • Ändern des Rückgabe-Wertes einer Methode
  • uvm.

Showcase zu Mutation Testing

Um die Funktionsweise noch besser zu demonstrieren, hier noch ein kleines Beispiel (Quelle):

function isDangerous(temp) {
if (temp > 1000) {
return true; //run away now!
} else {
return false; //it’s a very nice…
}
}

test(“test isDangerous”) {
expect(isDangerous(500)).toBeFalsy();
expect(isDangerous(2000)).toBeTruthy();
}

Der Test der oben angeführten Methode ergibt 100% Code Coverage, da alle Zeilen beim Test durchlaufen werden. Die Kill-Rate von Mutation Testing liefert jedoch keine so guten Ergebnisse. Ein erzeugter Mutant, bei dem das < auf ein <= geändert wird, wird nicht erkannt und die Tests laufen durch. Der Spezialfall einer Temperatur mit dem Wert 1000 wurde also nicht getestet. Mit dem folgenden Testfall kann man diesen Mutanten dann noch töten und es sind alle Spezialfälle der zu testenden Methode abgedeckt.

expect(isDangerous(1000)).toBeFalsy();

Fazit

Entwickler haben sich lange Zeit auf die Code-Coverage verlassen, um die Qualität ihrer Tests zu überprüfen. Jedoch liefert diese Metrik nicht immer die besten Ergebnisse und kann leicht in die Irre führen. Mutation Testing bietet eine verlässlichere Alternative, um die Qualität der Tests zu bestimmen. Außerdem ist es mit modernen Tools (z.B. http://pitest.org/) mittlerweile leicht möglich, Mutation Testing in bestehende Projekte zu integrieren.

Bildquellen

  • Code-Coverage_panther_736949_web: © Bildagentur PantherMedia / sebastien decoret