Eine Technik zur objektiven Bewertung der Testqualität

Mutation Testing mit Humbug und PHPUnit
Kommentare

Der Fokus auf Qualität in der entwickelten Software nimmt seit Jahren immer mehr zu. Das sollte sich idealerweise aber nicht nur auf die Software beschränken, sondern auch auf die erstellten Tests der Applikation. Mutation Testing ist dafür ein geeignetes Verfahren, um Tests automatisiert durchzuführen.

Mutation Testing – auch Mutationsanalyse oder Program Mutation Testing genannt – wird verwendet, um die Qualität der vorhandenen Tests zu evaluieren. Die Durchführung von Mutation Testing ist in der Theorie ziemlich einfach: Das vorhandene, zu testende Programm wird an einer Stelle verändert.

Jede so neu entstandene Version dieses Programms nennt man Mutant. Jeder erstellte Mutant wird mit den vorhandenen Tests überprüft, um zu verifizieren, dass sich die Testsuite anders verhält und man somit die Änderung an dem Programm bemerkt. Tritt dieser Fall ein, spricht man vom „killing the mutant“. Die existierende Testsuite wird anhand der prozentual entdeckten Mutanten bewertet. Je höher dieser Wert ist, desto genauer sind die automatisierten Tests in der Überprüfung und Validierung der Software.

Um diesen Richtwert zu erhöhen, können neue Tests erstellt werden, die genau so eine auftretende Mutation überprüfen. Mutationen basieren auf wohldefinierten Veränderungen (kleiner Ausschnitt der Veränderungen, Tabelle 1) wie zum Beispiel typischen Programmierfehlern (Verwendung des falschen Operators oder Rückgabewerts).

Mutatoren

Tabelle 1: Mutatoren

Zweck dieser ganzen Veränderungen ist es, den Softwareentwickler zu unterstützen, bessere und effektivere Tests zu schreiben. Ein wichtiger Aspekt ist die Identifizierung von Schwächen in den verwendeten Testdaten oder selten genutzten Funktionen. Zusätzlich soll es den Entwickler anspornen, nicht nur den „Happy-Path“ zu testen, sondern auch für die jeweiligen Methoden die Grenzwerte zu definieren und sie automatisiert zu testen. „Happy Path“ bezeichnet den Idealfall, wie eine Methode ausgeführt wird: Es werden keine Eventualitäten wie ungültige Parameter o. ä überprüft.

Warum sollte ich Mutation Testing anwenden?

Nicht immer sind die notwendigen Grenzwerte für einen Unit-Test direkt ersichtlich. Des Weiteren beschränken sich Entwickler gerne darauf, nur den „Happy-Path“ des Quellcodes zu testen. Durch Mutation Testing können solche unvollständigen Tests leicht gefunden und optimiert werden. Zusätzlich bekommt man durch Mutation Testing weitere Qualitätsmerkmale, mit denen man die vorhandene Software objektiv bewerten kann.

Soweit zur Theorie. Wie kann man das Ganze aber nun in der Praxis umsetzen? Wir nutzen dazu eine simple Vergleichsklasse, die wir zuerst mit einem Unit-Test versehen, um 100 Prozent Code-Coverage zu erreichen. Anschließend werden wir mit Humbug [2], einer Mutation-Framework-Implementierung für PHP, den vorhandenen Test analysieren und das Resultat von Humbug auswerten.

Erstellung eines kleinen Beispielskripts inkl. Unit-Test-Abdeckung

Zuerst erstellen wir eine Vergleichsklasse, die zwei Integer-Werte im Hinblick darauf vergleicht, ob der erste Parameter größer oder kleiner ist als der zweite. Diese Klasse sieht wie folgt aus:

<?php declare(strict_types=1);

class Comparison
{
  public function isGreaterThan(int $x, int $y): bool
  {
    return $x > $y;
  }

  public function isSmallerThan(int $x, int $y): bool
  {
    return $x < $y;
  }
}

Der notwendige Unit-Test, um 100 Prozent Code-Coverage zu erreichen, könnte z. B. folgendermaßen aussehen:

<?php declare(strict_types=1);

class ComparisonTest extends PHPUnit\Framework\TestCase
{
  private $comparison;

  public function setUp()
  {
    $this->comparison = new Comparison();
  }

  public function test_isGreaterThan()
  {
    self::assertTrue($this->comparison->isGreaterThan(5, 3));
  }

  public function test_isSmallerThan()
  {
    self::assertTrue($this->comparison->isSmallerThan(3, 5));
  }
}

Die von PHP Unit erstellten Testergebnisse sehen wie folgt aus:

OK (2 tests, 2 assertions)
Code Coverage Report:   
  2017-08-22 12:42:16   

Summary:
  Classes: 100.00% (1/1)
  Methods: 100.00% (2/2)
  Lines:   100.00% (2/2)

Comparison
  Methods: 100.00% (2/2)   Lines: 100.00% (2/2)

Was haben wir nun erreicht? Mit zwei vorhandenen Testmethoden haben wir eine Code-Coverage von 100 Prozent erreicht. Ob wir aber dabei alle möglichen Testszenarien berücksichtigen, sagt uns die Coverage nicht. In diesem Beispiel ist es natürlich ein Leichtes, die möglichen Grenzwerte zu bestimmen und die Tests entsprechend anzupassen. Die Grenzwerte für die erste Methode isGreaterThan() sind in diesem Beispiel x = 3 y = 5, x = 5 y = 4 und x = kleinster Integerwert, y = größter Integerwert.

Ausführung von Humbug und Auswertung der Testergebnisse

Wir führen Humbug einmal aus und schauen uns die erstellte Ausgabe an:

Humbug has completed the initial test run successfully.
Tests: 2 Line Coverage: 100.00%

Humbug is analysing source files...

Mutation Testing is commencing on 1 files...
(.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out)

M.M.

4 mutations were generated:
  2 mutants were killed
  0 mutants were not covered by tests
  2 covered mutants were not detected
  0 fatal errors were encountered
  0 time outs were encountered

Metrics:
  Mutation Score Indicator (MSI): 50%
  Mutation Code Coverage: 100%
  Covered Code MSI: 50%

Wie wir sehen können, führt Humbug initial unsere vorhandenen Tests aus. Anschließend analysiert Humbug unseren Quellcode. Darauf aufbauend verändert Humbug Kleinigkeiten in unserem Quellcode (Tabelle 1) und erstellt sogenannte „Mutanten“.

Nun wird für jede erstellte Mutation die Testsuite ausgeführt und geprüft, ob ein vorhandener Test fehlschlägt. Ist das der Fall, spricht man von einem gekillten Mutant und es wird dadurch bestätigt, dass die vorhandenen Tests eine ungewollte Veränderung der Logik feststellen. In den von Humbug generierten Logdateien können wir nun feststellen, welche Änderungen durch unsere Unit-Tests nicht gefunden oder sogar gar nicht abgedeckt sind (Listing 1).

1) \Humbug\Mutator\ConditionalBoundary\GreaterThan
Diff on \MutationTesting\Comparison::isGreaterThan()
--- Original
+++ New
@@ @@
  {
-        return $x > $y;
+        return $x >= $y;
  }

Optimierung der Unit-Tests basierend auf den Ergebnissen von Humbug

Um die von Humbug gefunden Mutationen zu testen, erweitern wir den Unit-Test wie folgt:

<?php declare(strict_types=1);

class ComparisonTest extends PHPUnit\Framework\TestCase
{
  // ...

  public function test_isGreaterThan()
  {
    self::assertTrue($this->comparison->isGreaterThan(5, 3));
    self::assertFalse($this->comparison->isGreaterThan(4, 4));
    self::assertTrue($this->comparison->isGreaterThan(PHP_INT_MAX, PHP_INT_MIN));
  }

  public function test_isSmallerThan()
  {
    self::assertTrue($this->comparison->isSmallerThan(3, 5));
    self::assertFalse($this->comparison->isSmallerThan(4, 4));
    self::assertTrue($this->comparison->isSmallerThan(PHP_INT_MIN, PHP_INT_MAX));
  }
}

und führen Humbug erneut aus:

Humbug has completed the initial test run successfully.
Tests: 2 Line Coverage: 100.00%

Humbug is analysing source files...

Mutation Testing is commencing on 1 files...
(.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out)

....

4 mutations were generated:
  4 mutants were killed
  0 mutants were not covered by tests
  0 covered mutants were not detected
  0 fatal errors were encountered
  0 time outs were encountered

Metrics:
  Mutation Score Indicator (MSI): 100%
  Mutation Code Coverage: 100%
  Covered Code MSI: 100%

Wie wir nun sehen, werden alle vier generierten Mutanten entdeckt und haben bei den neuen Metriken auch die 100 Prozent erreicht.

Fazit

Bei einer einzigen Klasse ist es für jeden Entwickler eine leichte Aufgabe, einen vernünftigen Unit-Test zu schreiben. Wie aber sieht es bei komplexeren Projekten mit tausenden Zeilen von Quellcode und entsprechend vielen einzelnen Klassen aus? In der Regel sind solche Tests nicht so vollständig umgesetzt wie in unserem kleinen Beispiel. Solche unvollständigen Tests zu finden, ist meist ein schwieriges und zeitintensives Unterfangen. Hierbei kann uns Humbug unterstützen.

Durch Mutation Testing entsteht die Chance, eine stabile und wertvolle Testsuite auch ohne konsequentes Test-driven Development zu erhalten. Humbug ist aktuell das Tool der Wahl für Mutation Testing im PHP-Umfeld. Leider unterstützt Humbug noch nicht die aktuellste PHP-Unit-Version. Übrigens: Ein alternatives Mutation-Testing-Tool ist Infection, das auf GitHub zur Verfügung steht und auf einem AST Tree basiert.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -