Suchen

Mit automatisierten Mutationstests die Testfallgüte prüfen

| Autor / Redakteur: Frank Büchner * / Sebastian Gerstl

Bei Bedenken hinsichtlich der Qualität Ihrer Testfälle könnten Mutationstests Abhilfe schaffen. Für Software, die in C/C++ geschrieben ist, geht dies erstmals sogar auf Knopfdruck.

Firmen zum Thema

Mutationstest-Prozess: Im Testwerkzeug TESSY lassen sich alle Aktivitäten automatissieren.
Mutationstest-Prozess: Im Testwerkzeug TESSY lassen sich alle Aktivitäten automatissieren.
(Bild: Hitex)

Voraussetzung für die Durchführung von Mutationstests sind eine Anzahl von bestandenen Testfällen für das Testobjekt, das beispielsweise eine Software-Unit beziehungsweise eine Funktion im Sinne von C sein kann. Beim Mutationstest wird die Software (das Testobjekt) verändert („mutiert“) und danach wird geprüft, ob die vorhandenen bestandenen Testfälle diese Mutation aufdecken. Im Fachjargon heißt dies, der Mutant wird durch den Testfall „getötet“. Sollten Mutanten alle Testfälle überleben, befinden die Testfälle auch geänderte Testobjekte (nämlich die mutierten) für in Ordnung. Das ist bedenklich und sollte näher untersucht werden.

Für eine Mutation sind der Phantasie keine Grenzen gesetzt, jedoch muss das Testobjekt syntaktisch korrekt (kompilierbar) bleiben. Für unseren Zweck sollen die Mutationen subtil sein, d.h. die Veränderung am Testobjekt soll gering sein, beispielsweise indem ‚<’ durch ‚<=’ ersetzt wird. (Dem liegt die Hypothese vom „kompetenten Software-Entwickler“ zu Grunde, von dem man annimmt, dass er zumindest „fast richtige“, jedoch keine „völlig falsche“ Software schreibt. Subtile Mutationen versuchen beispielsweise „off-by-one“-Fehler zu finden.)

Bildergalerie

Grobe Mutationen, beispielsweise das Entfernen eines größeren else-Zweigs, sind eher ungeeignet, denn auch unzureichende Testfälle bemerken dies wahrscheinlich. Typische Mutationen (das Fehlermodell) bei C/C++-Programmen sind die Verfälschung von logischen Ausdrücken (beispielsweise das Ersetzen eines logischen UNDs durch ein logisches ODER); die Verfälschung von arithmetischen Ausdrücken (beispielsweise die Addition eines konstanten Werts in einer Berechnung); die Manipulation von Variablen (beispielsweise Vertauschung von zwei Variablen); die Verfälschung von relationalen Operatoren (beispielsweise der Austausch von ‚<’ durch ‚>’); die Manipulation von Anweisungen (beispielsweise das Entfernen eines else-Zweigs oder das Einfügen einer return-Anweisung).

Die Mutanten, die wir im Folgenden betrachten, enthalten genau eine Mutation. Dem liegt die (empirisch bestätigte) Annahme [siehe Offut] zugrunde, dass ein Testfall, der einen Mutanten mit genau einer (subtilen) Abweichung tötet, auch Mutanten mit komplexeren Abweichungen töten würde (Kopplungseffekt).

TESSY, das Werkzeug zum automatisierten Modul-/Unit-Test von eingebetteter Software, kann Mutationstests auf Knopfdruck durchführen. Voraussetzung ist ein Testobjekt mit einer Menge von Testfällen, die alle bestanden sind. Solange noch nicht bestandene Testfälle für das Testobjekt vorhanden sind, ist Mutationstest sinnlos. TESSY kann logische und relationale Operatoren mutieren. Welche Mutationen genau vorgenommen werden, kann individuell festgelegt werden. Natürlich wirkt sich die Anzahl der vorzunehmenden Mutationen auf die Gesamtlaufzeit des Mutationstests aus.

Beispiel für die Mutation eines Testobjekts

Als Beispiel betrachten wir ein Testobjekt, für das drei bestandene Testfälle vorhanden sind (siehe Bild 2). Diese drei Testfälle erreichen 100% Coverage – sowohl für Zweigüberdeckung als auch für Modified Condition / Decision Coverage (MC/DC).

Nach der Ausführung des Mutationstests durch TESSY erhält man das in Bild 3 gezeigte Ergebnis. Auf der rechten Seite ist dort das Testobjekt dargestellt. Es besteht im Wesentlichen aus zwei if-Anweisungen. In der ersten if-Anweisung wird ‚<‘ als relationaler Operator in der Entscheidung verwendet; in der zweiten if-Anweisung ist es ‚>‘. Diese beiden Operatoren wurden von TESSY mutiert, und zwar gemäß der Standardeinstellung für die Mutation, wie auf der rechten Seite in Bild 1 zu sehen. Somit wird ‚<‘ aus der ersten if-Anweisung zu ‚<=‘ und ‚>‘ aus der zweiten if-Anweisung zu ‚>=‘.

Bild 3: 
Das Ergebnis des Mutationstests durch TESSY.
Bild 3: 
Das Ergebnis des Mutationstests durch TESSY.
(Bild: Hitex)

Bild 3 zeigt links oben das Ergebnis der Mutation von ‚<‘ zu ‚<=‘ in der ersten if-Anweisung. Diese Mutation bewirkte das Fehlschlagen eines Testfalls („Mutation caused test failure“), d.h. diese Mutation wurde getötet. Beim Mutationstest ist das Fehlschlagen eines Testfalls positiv, deshalb gibt es für diese Mutation ein grünes Häkchen als Resultat. Links unten in Bild 3 findet sich das Ergebnis der Mutation von ‚>‘ zu ‚>=‘ in der zweiten if-Anweisung. Diese Mutation wurde durch keinen Testfall aufgedeckt („Mutation survived all test cases“), d.h. diese Mutation wurde nicht getötet. Das ist bedenklich, deshalb gibt es das rote Kreuz als Resultat. Logische Operatoren kommen in diesem Testobjekt nicht vor, deshalb können sie auch nicht mutiert werden.

Wenn man wissen möchte, welche Testfälle Mutationen getötet haben (d.h. die adäquaten Testfälle), kann man den von TESSY ermittelten „Mutation Score“ ansehen. Der Mutation Score gibt das Verhältnis von getöteten zu der Zahl aller angewandten Mutationen an.

Bild 4: Der von TESSY ermittelte Mutation Score.
Bild 4: Der von TESSY ermittelte Mutation Score.
(Bild: Hitex)

In Bild 4 ist in der Spalte M der Mutation Score der 3 Testfälle dargestellt. Der Tooltip zeigt, dass Testfall 1 eine von zwei Mutationen getötet hat, was den Mutation Score von 50% ergibt. Weder Testfall 2 noch Testfall 3 haben einen Mutanten getötet. Dies kennzeichnet das rote Kreuz.

Am Beispiel haben wir gesehen, dass von zwei Mutationen nur eine von den drei Testfällen getötet wurden. Das hängt grundlegend mit der Qualität der Testfälle zusammen. Bei näherer Betrachtung von Testfallwerten und Quellcode erkennt man, dass die Werte von Testfall 1 so gewählt sind, dass es auf den relationalen Operator in der ersten if-Anweisung ankommt. Die Variable v1 hat im ersten Testfall den Wert 5 und die Variable r1.range_start ebenfalls. Damit lautet die Entscheidung in der ersten if-Anweisung ‚5 < 5‘, was „falsch“ ergibt. Durch die Mutation lautet die Entscheidung in der ersten if-Anweisung ‚5<=5‘, was „wahr“ ergibt. Dadurch liefert der erste Testfall aufgrund der Mutation ein nicht erwartetes Ergebnis („no“ anstelle des korrekten und erwarteten „yes“). Das Fehlschlagen des ersten Testfalls tötet also die Mutation.

Eigentlich ist dem dritten Testfall die Aufgabe zugedacht, die zweite Mutation zu töten. Leider sind aber die Werte des dritten Testfalls schlecht gewählt. Die Variable v1 hat den Wert 9 und r1.range_start+r1.range_len ergibt 5+2=7. Damit lautet die Entscheidung in der zweiten if-Anweisung im Original ‚9>7‘ und in der Mutation ‚9>=7‘. Beide Entscheidungen ergeben „wahr“. Somit liefert Original und Mutation dasselbe (korrekte) Ergebnis, nämlich „no“; der dritte Testfall ist sowohl für Original und Mutation bestanden und tötet die Mutation nicht. Oder etwas flapsiger ausgedrückt: Dem dritten Testfall ist es egal, wie der relationale Operator lautet; der dritte Testfall schaut nicht genau genug hin.

Der Unterschied in der Güte der beiden Testfälle 1 und 3 ergibt sich aus der Verwendung von Grenzwerten („boundary values“). Testfall 1 verwendet mit dem Wert 5 für die Variable v1 den Startwert des Bereichs (startet bei 5 und hat die Länge 2, umfasst also die Werte 5 und 6) und testet somit an einer Grenze. Testfall 3 testet mit dem Wert 9 für die Variable v1 nicht an einer Grenze des Bereichs. Betrachtung von Grenzwerten wird beispielsweise in der IEC 26262:2018 in Tabelle 8 als Methode genannt, wie man zu Testfällen für den Software-Unit-Test kommen kann. Die Methode 1c in dieser Tabelle heißt „Analysis of boundary values“ und ist empfohlen für ASIL A und besonders empfohlen für ASIL B bis D. Die IEC 61508 nennt „boundary value analysis“ beispielsweise in Tabelle B.2 und B.3 von Teil 3. In beiden Tabellen ist diese Methode empfohlen für SIL 1 und besonders empfohlen für SIL 2 bis 4.

Mit Hilfe eines Mutationstests kann man auch Testfallmengen bewerten. Eine Testfallmenge heißt adäquat, wenn sie alle Mutanten aufdeckt. Von zwei adäquaten Testfallmengen ist natürlich diejenige mit weniger Testfällen vorzuziehen. Dies ermöglicht auch den Vergleich von Testfallkonstruktionsverfahren.

Mutationen können zu Endlosschleifen führen

Durch eine Mutation kann eine Endlosschleife entstehen. Dies tötet die Mutation. Allerdings muss die Endlosschleife festgestellt, abgebrochen und danach der Mutationstestprozess fortgesetzt werden. TESSY kann Tests bei Überschreiten einer bestimmten Ausführungszeit automatisch abgebrechen.

Bild 5: Eine Mutation, die zu einer Endlosschleife führt, tötet den Mutanten.
Bild 5: Eine Mutation, die zu einer Endlosschleife führt, tötet den Mutanten.
(Bild: Hitex)

Im in Bild 5 gezeigten Beispiel wurde die Funktion count() mit einem Testfall getestet. Dieser Testfall hat den Eingabewert 10 für den Parameter x und mit dem Return-Wert 1 das korrekte Ergebnis. Dieser Testfall tötet alle vier auf der linken Seite von Bild 5 angegebenen Mutationen. Allerdings wird die dritte Mutation (von ‚<=‘ zu ‚>=‘) nicht durch Fehlschlagen des Testfalls getötet, sondern es entsteht eine Endlosschleife. TESSY ist so eingestellt, dass Endlosschleifen nach 10 Sekunden Ausführungszeit automatisch abgebrochen werden. Dies ist für die dritte Mutation eingetreten und TESSY bewertet diese Mutation als getötet. Danach wird noch die vierte Mutation durchgeführt.

Äquivalente Mutanten sind ein Problem

Bei der Mutation können äquivalente Mutanten entstehen. In diesem Fall unterscheidet sich das nach außen sichtbare Programmverhalten nicht (schwache Mutation); Original und Mutant sind funktional äquivalent. In der Praxis muss ein Mensch entscheiden, ob Original und Mutant funktional äquivalent sind. Hilfreich für die Untersuchung ist hierbei, wenn lediglich jeweils eine einzige Mutation vorgenommen wird, was beim Mutationstest mit TESSY der Fall ist. Äquivalente Mutanten sind ein Schwachpunkt des Mutationstests.

Bild 6: 
Die Mutation von ‘<’ zu ‘<=’ wird nach außen nicht sichtbar.
Bild 6: 
Die Mutation von ‘<’ zu ‘<=’ wird nach außen nicht sichtbar.
(Bild: Hitex)

Bild 6 stellt ein Beispiel für eine äquivalente Mutation dar. Für die Mutation des relationalen Operators ‚<‘ zu ‚<=‘ ergibt sich für den Eingabewert 0 zwar ein anderes internes Programmverhalten (in der Mutation wird der then-Zweig durchlaufen, im Original nicht), der Rückgabewert ist jedoch in beiden Fällen der korrekte Wert 0, d.h. das veränderte interne Programmverhalten ist nach außen nicht sichtbar (schwache Mutation).

Ergänzendes zum Thema
Typische Fachbegriffe bei Mutationstests

Fault Injection: In nicht mutiertem Testobjekt werden Fehler von außen injiziert, um die Robustheit zu prüfen.

Fehlermodell (Fault model): Die Kategorien der möglichen Mutationen. Diese sind natürlich bei einem C Programm als Testobjekt anders als z.B. in einem Zustandsdiagramm. Ein Beispiel für ein Fehlermodell für Testobjekte in der Programmiersprache C ist im Artikel angegeben.

Fehlereinpflanzung (Error Seeding): In der IEC 61508 heißt der Mutationstest „Fehlereinpflanzung“ bzw. „Error Seeding“.

Kopplungseffekt (coupling effect): Wird ein Mutant mit einer einzelnen Mutation durch eine Testfallmenge entdeckt, so werden auch mehrfache Mutationen entdeckt.

Starker Mutationstest (Strong mutation test): Der Mutant wird nur von außen betrachtet (black box) und der Mutant wird nur durch einen Testfall entdeckt, der nach außen ein anderes Ergebnis liefert als das Original.

Schwacher Mutationstest (Weak mutation test): Ein Testfall sorgt im Innern des Mutanten einen anderes Verhalten als beim Original. Dieses andere Verhalten wird jedoch nicht nach außen sichtbar.

Adäquater Testfall / adäquate Testfallmenge: Ein Testfall heißt adäquat, wenn er einen Mutanten tötet. Eine Testfallmenge heißt adäquat, wenn sie alle Mutanten tötet.

Mutation Score: Das Verhältnis der getöteten Mutanten zur Zahl aller Mutanten. Üblicherweise in Prozent angegeben.

Mutationstests in Standards wie IEC 61508 oder ISO 26262

Die IEC 61508 bezeichnet den Mutationstest als „Durchführung von Testfällen nach Fehlereinpflanzung“ und empfiehlt dies für Safety Integrity Level (SIL) 2 bis 4 (in Tabelle B.2 von Teil 3).

Die IEC 61508 führt auch aus (in Abschnitt C.5.6 von Teil 7), dass man aus der Anzahl der Fehler, die eine Testsuite in einem originalen Testobjekt entdeckt, und der Zahl der Mutationen, die diese Testsuite entdeckt, eine Abschätzung für die Gesamtzahl der im Testobjekt vorhandenen Fehler finden kann (prädizierend). Das Verhältnis der erkannten Mutanten zur Gesamtzahl der Mutanten ist gleich dem Verhältnis der gefundenen Fehler im originalen Testobjekt zu der Gesamtzahl der Fehler im originalen Testobjekt. Diese Abschätzung setzt natürlich die gleiche statistische Verteilung von Arten und Positionen der Mutationen und der tatsächlichen Fehler voraus; wenn beispielsweise die tatsächlichen Fehler fehlerhafte Berechnungen sind, jedoch keine arithmetischen Mutationen verwendet werden, wird die Abschätzung kaum zutreffen.

Die ISO 26262:2018 erwähnt Mutationstest lediglich in einer Fußnote zur Methode „Fault Injection Test“ in Tabelle 7 von Teil 6, die Methoden zur Software-Unit-Verifikation aufführt und in Tabelle 10, die Methoden zur Verifikation der Software-Integration aufführt. Allerdings werden hier „code mutations“ in einen Topf mit Verfahren des Robustheitstests wie das willkürliche Verändern (Korrumpieren) von Datenspeicher oder CPU-Registern geworfen.

Durch das Korrumpieren beispielsweise von Werten von Variablen im Datenspeicher liest das Testobjekt nicht die Werte zurück, die dort vorher abgespeichert war und das Testziel dieser Fault Injection ist es, herauszufinden, ob das Testobjekt damit zurechtkommt. Konkreter formuliert: Erkennt (beispielsweise aufgrund redundanten Speicherung der Werte) das Testobjekt beispielsweise einen Bitflip durch kosmische Strahlung und reagiert darauf geeignet? Eine solche Fragestellung unterscheidet sich fundamental vom Ziel des Mutationstests, nämlich die Qualität der Testfälle zu bewerten und durch bessere Testfälle möglicherweise Programmierfehler aufzudecken.

Fazit: Mutationstests sorgen für höhere Softwarequalität

Mutationstest haben einen großen Nutzen, denn sie decken unzureichende Testfälle auf, die daraufhin verbessert werden können, was in der Folge die Qualität der getesteten Software sichert. Mit TESSY können Mutationstest ohne weiteren Aufwand auf Knopfdruck durchgeführt werden.

Diesen Beitrag lesen Sie auch in der Fachzeitschrift ELEKTRONIKPRAXIS Ausgabe 19/2020 (Download PDF)

Der Autor

Frank Büchner, Diplom-Informatiker und Senior Test Engineer bei Hitex.
Frank Büchner, Diplom-Informatiker und Senior Test Engineer bei Hitex.
(Bild: Hitex)

* Frank Büchner hat ein Diplom in Informatik von der Technischen Hochschule Karlsruhe. Seit mehreren Jahren widmet er sich dem Thema Testen und Software-Qualität. Seine Kenntnisse vermittelt er regelmäßig durch Vorträge und Fachartikel. Momentan arbeitet er als „Principal Engineer Software Quality“ bei der.Hitex GmbH in Karlsruhe.

Literatur

[61508] IEC 61508:2010

[26262] ISO 26262:2018

[Liggesmeyer] Liggesmeyer, Peter: Software-Qualität: Testen, Analysieren und Verifizieren von Software. 2. Auflage, Heidelberg, Berlin, 2009. Spektrum Akademischer Verlag.

[Hoffmann] Dirk W. Hoffmann, Software-Qualität. Springer-Verlag Berlin Heidelberg, 2008.

[Offut] A. Jefferson Offut, Clemson Univeristy: Investigations of the software testing coupling effect, in ACM Transactions on Software Engineering and Methodology, New York, Volume 1 Issue 1, Jan. 1992.

(ID:46871680)