Suchen

Test- und Qualitätssicherung Mutationstest – ein Plädoyer für eine vernachlässigte Testtechnik

Autor / Redakteur: Frank Büchner * / Franz Graser

Softwareentwickler und Tester, die von Zweifeln hinsichtlich der Qualität ihrer Tests beschlichen werden, können ihre Bedenken vielleicht mit einem Mutationstest ausräumen.

Firma zum Thema

Funktionsschema: Ablauf des Mutationstests mit dem Werkzeug Tessy. Beim Mutationstest wird die Software verändert. Danach wird geprüft, ob die Mutation von den vorhandenen Testfällen aufgedeckt wird.
Funktionsschema: Ablauf des Mutationstests mit dem Werkzeug Tessy. Beim Mutationstest wird die Software verändert. Danach wird geprüft, ob die Mutation von den vorhandenen Testfällen aufgedeckt wird.
(Bild: Hitex)

Bei einem Mutationstest wird die Software (beziehungsweise das Testobjekt) verändert („mutiert“). Danach wird geprüft, ob die vorhandenen Testfälle diese Mutation aufdecken. Im Fachjargon heißt dies: Der Mutant wird durch den Testfall „getötet“. Ein Testfall, der einen Mutanten erkennt, heißt adäquat. Für eine Mutation sind der Phantasie keine Grenzen gesetzt, jedoch muss das Testobjekt syntaktisch korrekt, also kompilierbar, bleiben. Für unseren Zweck sollten die Mutationen jedoch ziemlich subtil sein.

Typische Mutationen bei C-Programmen (das Fehlermodell) sind die Verfälschung von logischen Ausdrücken (beispielsweise das Ersetzen eines logischen UND durch ein logisches ODER), die Verfälschung von arithmetischen Ausdrücken (etwa die Addition eines konstanten Werts in einer Berechnung), die Manipulation von Variablen (beispielsweise Vertauschung von zwei Variablen), die Verfälschung von relationalen Operatoren (beispielsweise ‚<’ durch ‚>’ ersetzen) oder 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 [6] zugrunde, dass wenn ein Testfall einen Mutanten mit genau einer (subtilen) Abweichung erkennt, dieser Testfall auch Mutanten mit komplexeren Abweichungen töten würde (Kopplungseffekt). Außerdem reduzieren wir dadurch den Aufwand, der notwendig ist, um zu entscheiden, ob durch die Mutation ein Mutant entstanden ist, der nicht getötet werden kann. In diesem Fall unterscheidet sich das nach außen sichtbare Programmverhalten nicht. Original und Mutant sind funktional äquivalent. In der Praxis muss ein Mensch entscheiden, ob Original und Mutant funktional äquivalent sind.

Vorhandene Testfälle auf die Mutanten anwenden

Um einen Mutationstest vorzunehmen, muss man zwei Dinge tun: Man muss einen Mutanten erzeugen und man die vorhandenen Testfälle auf diesen Mutanten anwenden. Weil wir nur wenige stichprobenartige Mutanten erzeugen wollen, benötigen wir hierzu kein Werkzeug. Wir können dies leicht manuell erledigen. Aufwendiger ist die Testdurchführung. Wenn wir von 20 Testfällen und 10 Mutanten ausgehen, müssen 200 Testfälle ausgeführt werden. Das macht die Automatisierung der Testausführung in der Praxis notwendig. In unserem Fall verwenden wir das Unit-Testwerkzeug Tessy [7]. Als erstes Beispiel betrachten wir die kleine Funktion shiftC().

( Hitex)

Der Testreport von Tessy: Zwei Testfälle liefern das erwartete Ergebnis.
Der Testreport von Tessy: Zwei Testfälle liefern das erwartete Ergebnis.
(Bild: Hitex)

Die Tabelle zeigt das Testergebnis für zwei Testfälle, die mit Tessy auf dem Original-Quellcode von shiftC() ausgeführt wurden. Beide Testfälle liefern das richtige (erwartete) Ergebnis, und Tessy ermittelt für diese beiden Testfälle zusammen eine Zweigüberdeckung von 100%. Das heißt: Sowohl der then-Zweig als auch der else-Zweig der if-Anweisung werden ausgeführt. Und da unsere beiden Testfälle nicht trivial sind, sollten wir eigentlich fertig sein.

Mutationen schaffen Gewissheit

Der Testreport von Tessy: Zwei Testfälle liefern das erwartete Ergebnis.
Der Testreport von Tessy: Zwei Testfälle liefern das erwartete Ergebnis.
( Hitex)

Um uns zu vergewissern, können wir unser Testobjekt mutieren. Viele der möglichen Mutationen würden durch unsere beiden Testfälle aufgedeckt, so etwa, wenn die Entscheidung „(A && B)“ zu „(C && B)“ verändert wird. Hier schlägt der erste Testfall fehl. Ebenso würde erkannt, wenn in der Entscheidung das logische UND durch ein logisches ODER ersetzt wird.

Wird jedoch in der Entscheidung die Variable B durch C ersetzt, so wird diese Mutation nicht erkannt:

( Hitex)

Wird in der Entscheidung das logische UND (&&) durch ein bitweises UND (&) ersetzt, wird diese Mutation ebenfalls nicht erkannt:

( Hitex)

Beide nicht erkannten Mutationen sind subtil, verändern jedoch das Verhalten des Testobjekts signifikant und können durch einen Denk- oder auch einen Flüchtigkeitsfehler relativ leicht entstehen.

Um den zweiten Mutanten zu töten, reicht es aus, die Werte eines der beiden bestehenden Testfälle zu ändern. Zum Beispiel könnte man im zweiten Testfall den Wert 2 für B verwenden (aber nicht 5).

Um beide Mutanten nachhaltig zu töten (das heißt, ohne dass unmittelbar spiegelbildliche Mutationen möglich werden, die nicht getötet würden), benötigt man einen dritten Testfall, in dem die Variable B den Wert 0 annimmt. Die Tabelle 2 im Kasten links zeigt die Resultate. Mit den drei Testfällen in der Tabelle 2 werden beide Mutanten erkannt. Durch die Verwendung von drei Testfällen wird überdies der Code-Überdeckungsgrad von 100% Zweigüberdeckung auf 100% MC/DC (Modified Condition/Decision Coverage, ein Kriterium des Standards DO-178B) erhöht.

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. Man kann die Idee des Mutationstests zur Reduzierung der normalerweise großen Zahl von automatisch generierten Testfällen verwenden [5]: Ein Testfall muss mindestens einen Mutanten töten, der bisher noch von keinem anderen Testfall getötet wurde, um nicht verworfen zu werden.

Mutationstests aus Sicht der Norm IEC 61508

Die Norm 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.

Die IEC 61508 führt auch aus, dass man aus der Anzahl der Fehler, die eine Testsuite in einem originalen Testobjekt entdeckt, und der Zahl der Mutationen, die diese Testsuite ermittelt, 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 den 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.

Obwohl sich das äußere Programmverhalten des Mutanten gegenüber dem Original nicht ändert, kann es sein, dass ein Testfall im Innern des Mutanten einen anderen Programmablauf erzeugt als im Original. In diesem Fall spricht man von schwachem Mutationstest.

( Hitex)

Die Mutation von ‘<’ zu ‘<=’ wird nach außen nicht sichtbar.

Der Unterschied zur Fault Injection

Nicht zu verwechseln mit dem Mutationstest ist die Fault Injection, die beispielsweise in der Norm ISO 26262 [2] als Testmethode erwähnt wird. Bei einer Fault Injection bleibt das Testobjekt in seinem Originalzustand, es wird nicht mutiert. Der Fehler wird außerhalb des Testobjekts erzeugt. Wenn das Testobjekt Software ist, wäre beispielsweise das Verändern (Korrumpieren) des Speichers für die Variablen während der Ausführung der Software eine Fault Injection. Die Software liest also aus einer Variablen nicht den Wert zurück, den sie vorher abgespeichert hat. Die Frage ist, ob dies Auswirkungen hat und ob die Software mit einer solchen Situation umgehen kann.

Fault Injection ist also ein Robustheitstest, zum Beispiel bezüglich der Auswirkungen von Bitflips durch kosmische Strahlung. Durch eine Fault Injection kann auch der Programmspeicher des Testobjekts (willkürlich) geändert (korrumpiert) werden. Dies sehen wir jedoch nicht als Mutation, die den Sinn hat, Programmierfehler im Quellcode aufzudecken, sondern vielmehr als Robustheitstest.

Literaturhinweise:

[1] IEC 61508, Teil 3, Tabelle B.2 = Empfehlung Fehlereinpflanzung sowie Teil 7, C.5.6 = Beschreibung Fehlereinpflanzung

[2] ISO/FDIS 26262, Teil 6, Tabelle 10 und Tabelle 13 -> Fault Injection Test

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

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

[5] Wolfgang Herzner, Rupert Schlick, Harald Brandl, Johannes Wiesalla: Towards Fault-based Generation of Test Cases for Dependable Embedded Software, in Softwaretechnik-Trends, Gesellschaft für Informatik e.V., Band 31, Heft 3, August 2011.

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

[7] Tessy: http://www.hitex.de/tessy

* Frank-Büchner ist Diplom-Informatiker und arbeitet als Senior Test Engineer bei Hitex Development Tools in Karlsruhe.

(ID:33431280)