Die Idee hinter den Mutationstests von Testfällen ist, absichtlich Fehler in die Software einzubringen und diese mutierte Software dann den Tests zu unterziehen. Im Falle einer Embedded-Anwendung wird der zu testende C/C++-Code leicht verändert („mutiert“), so dass typische Programmierfehler vorliegen. Wenn die eingesetzten Unit- und Integrationstests die Mutation erkennen, werden sie als „nützlich“ bewertet. Diese Methode deckt schwache Testfälle auf und gibt Hinweise, wo sich die Testfälle verbessern lassen. Das hilft dabei, eine bessere Testqualität zu erreichen und damit die Anforderungen der Standards zu erfüllen.

Aktuelle Ansätze: Was prüft der Test tatsächlich?

Bild 1: Der Test prüft einen Ausgangswert, der nicht durch das Testobjekt aktualisiert wurde.

Bild 1: Der Test prüft einen Ausgangswert, der nicht durch das Testobjekt aktualisiert wurde. Razorcat

Die Messung der Code-Abdeckung ist einer der in der Praxis am häufigsten verwendeten Qualitätsmaßstäbe. Die Abdeckung zeigt, welche Teile des Codes während des Tests ausgeführt wurden. Sie liefert aber keine Informationen darüber, was getestet wurde, sondern nur darüber, was nicht getestet wurde. Folglich ist eine 100-prozentige Code-Abdeckung zwar obligatorisch, aber als Qualitätsmaßstab für eine Testsuite nicht ausreichend.

Es ist möglich, Tests für ein Testobjekt zu erstellen, ohne ein Ergebnis zu prüfen. Dabei sollte zumindest die Prüfung irgendeines Ausgangswerts, einer Sequenz an Funktionsaufrufen (Function Calls) oder andere Verifikationsmöglichkeiten zum Einsatz kommen. Beim Refactoring von Tests können Änderungen dazu führen, dass alle Verifikationen entfernt werden und somit schließlich ein leerer Test vorliegt. Eine automatisierte Evaluierung der Tests sollte solche Mängel bei der Ausführung der Tests erkennen.

Sind Ausgangswerte berechnet oder zufällig?

Das Beispiel in Bild 1 zeigt eine schwache Testdefinition: Da das Testobjekt nicht jedes Mal einen neuen Wert für die Ausgangsvariable berechnet, ist das Ergebnis zufällig – hier wiederholte sich das Ergebnis des vorherigen Testfalls.

Aus diesem Grund erfolgt die Initialisierung der Ausgangswerte von Tests normalerweise manuell mit einem ungültigen Wert, um zu sehen, ob das Ergebnis vom Testobjekt erstellt wurde. Der Nachteil eines solchen Ansatzes besteht darin, dass es beim Test Eingangswerte gibt, die nur technisch notwendig sind aber von den eigentlichen Testaspekten ablenken.

Eine automatisierte Initialisierung solcher Ausgangswerte mit unterschiedlichen Patterns würde dabei helfen, die Testdefinition von technischen Artefakten beim Test zu bereinigen und das Augenmerk mehr auf die testrelevanten Aspekte zu lenken.

Erkennt der Test unbeabsichtigte Codeänderungen?

Bild 2: Mutierter Code, der immer noch die gleichen Testergebnisse liefert.

Bild 2: Mutierter Code, der immer noch die gleichen Testergebnisse liefert. Razorcat

Vor fast 50 Jahren schlug R. Lipton in der Publikation „Fault Diagnosis of Computer Programs“ vor, eine Modifikation des zu testenden Codes vorzunehmen, um einen Fehler einzuschleusen, und dann die Testsuite erneut auszuführen. Wenn die Testsuite fehlschlägt, war sie in der Lage, diesen Fehler zu erkennen. Das Beispiel in Bild 2 zeigt eine leichte Mutation des Testobjekts, die vorhandene Tests nicht erkannten, obwohl die Tests 100 Prozent der Codezweige abdeckten. Das deutet darauf hin, dass Tests fehlen.

In neueren Veröffentlichungen wird diese Idee aufgegriffen: Mutationstests gelten als eine mögliche Lösung für solche Fragestellungen. Problematisch waren in der Vergangenheit die fehlende Werkzeugunterstützung und die hohe Anzahl von Mutationen. Aktuelle Tools lösen dieses Problem durch Parallelisierung und bieten Mutationstests für Programmiersprachen wie JAVA, C/C++ oder C# an. Jedoch unterstützt kein Tool die Ausführung mit Compilern und Zielplattformen speziell für Embedded-Software.

Mutationstest: Begriffe und Definitionen

Bei der Modifikation von Programmen sind der Fantasie keine Grenzen gesetzt, jedoch muss der Code syntaktisch korrekt bleiben, um ihn in den Tests ausführen zu können. Typische Mutationen in C-Programmen sind Änderungen in logischen Ausdrücken (zum Beispiel Ersetzen eines logischen UND- Operators durch einen logischen ODER-Operator), die Modifikation von arithmetischen Ausdrücken (zum Beispiel Hinzufügen eines konstanten Wertes zu einem vorgegebenen Ausdruck) und die Manipulation von Variablen (zum Beispiel Austausch von zwei lokalen Variablen). Auch Änderungen im Programmablauf sind möglich (beispielsweise durch das Entfernen eines ELSE-Zweigs oder das Einfügen einer RETURN-Anweisung).

Jede Anwendung eines Mutationsoperators erzeugt einen Mutanten. Mutanten, die sich wie ein nicht modifizierter Code verhalten heißen dabei äquivalente Mutanten. Nach der Ausführung der Testsuite liegen folgende Ergebnisse vor:

  • Beseitigte Mutanten sind solche, die durch einen fehlgeschlagenen Test entdeckt wurden. Das ist ein guter Testfall, da Code-Modifikation aufgefallen sind.
  • Verbliebene Mutanten sind solche, die keinen fehlgeschlagenen Test hervorgebracht haben. Die Testsuite war nicht in der Lage, die Modifikation zu erkennen. Entweder weisen die Tests Schwächen auf oder es handelt sich um einen äquivalenten Mutanten. Hier ist nun eine manuelle Untersuchung notwendig.
  • Für jeden Test lässt sich ein Mutationswert berechnen (Prozentsatz beseitigter Mutanten im Vergleich zur Gesamtzahl an Mutanten).

Alle Tests mit einem Mutationswert von Null weisen auf potenzielle Schwächen hin und sind einer Prüfung zu unterziehen. Ein Mutationstestzyklus lässt sich gut in eine testgetriebene Entwicklung integrieren, wenn automatisierte Mutationstests zum Einsatz kommen. Die Mutationsanalyse sollte in der Entwicklung Teil einer Feedback-Schleife sein.

Wirksamkeit von Mutationstests

Bild 3: Automatisierter Entwicklungs- und Testzyklus mit Mutationsanalyse.

Bild 3: Automatisierter Entwicklungs- und Testzyklus mit Mutationsanalyse. Razorcat

Mutationstests basieren auf künstlich eingebrachten Fehlern. Doch was bedeutet das nun im Hinblick auf echte Fehler? Eine Hypothese besagt, dass „Programmierer im Allgemeinen kompetent genug sind, um einen Code zu produzieren, der zumindest fast richtig ist“. Die meisten von Programmierern eingebrachten Fehler sind kleine syntaktische Fehler. Und genau so funktioniert ein Mutationstest: Er wendet eine Mutation nach der anderen an und verändert den ursprünglichen Code nur geringfügig.

Auch der Kopplungseffekt gilt: Wenn die Testsuite eine einzige Mutation erkennt, kann sie auch mehrere Mutationen erkennen. Daher ist es nicht notwendig, die Tests mit kombinierten Mutationen durchzuführen. Außerdem kann die gleichzeitige Anwendung mehrerer Mutationen dazu führen, dass eine Mutation durch die andere verborgen bleibt, was zu einem äquivalenten Mutanten führen würde, der zu beseitigen ist.

Probleme und Herausforderungen

Die beiden hauptsächlichen Probleme des Mutationstestkonzepts resultieren aus der hohen Anzahl an Mutanten für eine vorgegebene Codebasis und dem Erzeugen äquivalenter Mutanten. Aufgrund der sehr langen Testdauer bei einer großen Anzahl an Mutanten ist es notwendig, die Ausführung der Mutanten zu optimieren, zum Beispiel durch Parallelisierung der Testausführung. Auch die Iterative Ausführung der Mutationstests für geänderte Codeteile ist sinnvoll, um ein schnelles Feedback zu erhalten.

Mutationstests in Normen

Eck-Daten

Mutationstests decken schwache Testfälle auf und geben Hinweise darauf, wo Verbesserungen beim Test von sicherheitskritischer Embedded-Software notwendig sind. Der Mutationstest basiert dabei auf künstlich eingebrachten Fehlern in den Code, deckt unzureichende Tests auf und erkennt auch potenzielle Fehler in der Software. Da sich Teile des Reviews von Tests automatisieren lassen, zum Beispiel innerhalb des Unittesttools Tessy für C/C++-Code, spart der Entwickler dabei wertvolle Zeit.

Die Norm IEC 61508 empfiehlt Mutationstests für Sicherheitsintegritätsgrade von SIL 2 bis SIL 4. Sie besagt auch, dass es möglich ist, die Anzahl der verbleibenden Fehler in einer Software nach der Anwendung von Mutationstests anhand der Anzahl der verbliebenen Mutanten im Verhältnis zur Gesamtzahl der Mutanten zu schätzen. Der Mutationstest darf aber nicht mit einem Fehlerinjektionstest verwechselt werden, der in der Norm ISO 26262 als Prüfmethode empfohlen wird und nur eine Art von Robustheitstest darstellt.

Fazit

Die Anwendung von Mutationstests kann schwache oder unzureichende Tests aufdecken. Außerdem kann sie potenzielle Fehler in der Software aufdecken und spart Zeit, weil sich Teile des Reviews von Tests automatisieren lassen

Wichtig hierfür ist die Verfügbarkeit von Tools, um für vorhandene Tests automatisch Mutationen zu erstellen und auszuführen. Innerhalb des Unittesttools Tessy für C/C++-Code von Razorcat ist der Mutationstest aller bestandenen Tests eine der zusätzlichen Optionen für die automatische Testausführung. Auch die Initialisierung von Ausgangsvariablen mit unterschiedlichen Testdaten-Mustern und die Prüfung des Vorhandenseins von mindestens einer Verifikation innerhalb eines Tests lässt sich bei allen ausgeführten Tests automatisch anwenden. Diese Optionen ermöglichen die automatische Qualitätsmessung von auf Embedded-C/C++-Compilern und Zielplattformen ausgeführt Tests.