Coverage zusammenführen Mit weniger Tests zu 100 Prozent Code-Coverage

Von Frank Büchner* 10 min Lesedauer

Anbieter zum Thema

In den Standards zur Entwicklung von sicherheitskritischen Systemen wie IEC 61508 oder ISO 26262 basiert die Software-Entwicklung auf dem V-Modell. In diesem Modell ist der Modul- oder Unit-Test die erste Testaktivität, die Nächste ist der Integrationstest. Bei beiden Testaktivitäten soll die Code-Coverage gemessen werden. Beim strikten Vorgehen nach dem V-Modell fällt Testaufwand an, der gegebenenfalls vermieden werden kann.

Das V-Modell gibt die Reihenfolge der Testaktivitäten vor.(Bild:  Hitex GmbH)
Das V-Modell gibt die Reihenfolge der Testaktivitäten vor.
(Bild: Hitex GmbH)

Der Modul- oder Unit-Test ist die erste Testaktivität im V-Modell, ganz unten auf der rechten, aufsteigenden Seite des Vs. Je nach Standard hat diese Aktivität unterschiedliche Bezeichnungen. Beispielsweise wird in IEC 61508 [1] diese Aktivität Modul-Test genannt, in ISO 26262 [2] und IEC 62304 [3] heißt sie (Software-) Unit-Verifikation. Dies sind einfach unterschiedliche Bezeichnungen für dieselbe Aktivität; im Folgenden reden wir vom (Software-) Unit-Test. Diese Testaktivität kann statisch oder dynamisch sein.

Bei statischer Software-Analyse wird der Quellcode untersucht, manuell oder mit Hilfe von Werkzeugen, um beispielsweise die Einhaltung von Codier-Regeln zu prüfen. Dies wird im Folgenden nicht weiter betrachtet. Beim dynamischen Software-Unit-Test wird das Testobjekt, die Software-Unit, übersetzt und ausgeführt. Hierbei kann man nicht nur die Ergebnisse bzw. das gewünschte Verhalten überprüfen, sondern auch die Code-Coverage (Code-Überdeckung) messen. IEC 61508 und ISO 26262 empfehlen dies explizit und benennen auch die zu messenden Maße, je nach Kritikalität der Software. Die IEC 61508 nennt zudem explizit den zu erreichenden Wert 100 Prozent.

Unit-Testen nach der reinen Lehre

Beim Unit-Testen nach der reinen Lehre werden die zu testenden Units isoliert vom Rest der Applikation getestet. Falls die zu testende Unit andere Units aufruft, heißt isoliert in diesem Fall konkret, dass nicht die vorhandenen Implementierungen der aufgerufenen Units verwendet werden, sondern dass diese durch sogenannte Stubs (manchmal auch als Mocks bezeichnet) ersetzt werden. Die Isolation sorgt also dafür, dass der vorhandene Code der aufgerufenen Units die Ergebnisse des Unit-Tests nicht beeinflusst. Im Fall von eingebetteter Software gibt es auch keine Beeinflussung der zu testenden Unit durch die Hardware. Man kann durch einen Stub gezielt fehlerhafte Werte an die aufrufende Unit übergeben, um herauszufinden, wie sie damit zurechtkommt (fault injection).

Man führt nun Testfälle aus und misst die dadurch erreichte Code-Coverage. Bei der Coverage-Messung wird ermittelt, welche Bestandteile des Codes, beispielsweise Anweisungen (Statements) oder Zweige (Branches) bei den Tests durchlaufen wurden. Sind alle Bestandteile des Testobjekts durch die Tests durchlaufen worden, hat man 100 Prozent Code-Coverage erreicht. Es versteht sich von selbst, dass alle Tests bestanden sein müssen. Natürlich kann man daraus nicht schlussfolgern, dass nun keine Fehler mehr im Code vorhanden sind; man weiß lediglich, dass es keine Code-Bestandteile mehr gibt, die gar nicht getestet wurden.

Nach Meinung des Autors ist 100 Prozent Code-Coverage eine notwendige, jedoch keine hinreichende Bedingung für das Ende der Tests. Ist der Test einer Unit beendet, nimmt man sich die nächste vor und testet auch diese isoliert vom Rest der Applikation, bis alle Code-Bestandteile durchlaufen sind und insgesamt die Tests als ausreichend angesehen werden. Sind alle Units getestet, ist die Testaktivität des dynamischen Software-Unit-Tests beendet.

Integrationstest von Units mit Aufrufbeziehung

Testet man eine Unit und ersetzt dabei aber nicht die aufgerufenen Units durch Stubs, so wie beim Unit-Test nach der reinen Lehre, sondern verwendet die Implementierung der aufgerufenen Unit aus der Applikation, ist dies de facto ein Integrationstest, auch wenn die Technik des Unit-Tests verwendet wird. Denn aufrufende und aufgerufene Unit werden zusammen getestet und ein guter Testfall für die aufrufende Unit sollte nur bestehen, wenn die aufgerufene Unit sich so verhält, wie es die aufrufende Unit bei diesem Testfall erwartet. Beim Test der aufrufenden Unit kann man auch die Coverage der aufgerufenen Unit messen. Wurde die aufgerufene Unit zuvor dem Unit-Test unterzogen und dabei die Code-Coverage gemessen, so werden zwangsläufig Code-Bestandteile der aufgerufenen Unit doppelt gemessen, einmal im Unit-Test der aufgerufenen Unit und einmal im Test der aufrufenden Unit, d.h. im Integrationstest beider Units.

Mit dem Integrationstest beginnen

Kann man technisch die Code-Coverage von verschiedenen Units aus den Testaktivitäten Unit- und Integrationstest zusammenführen, könnte man den ganzen Testprozess mit dem Integrationstest anstelle des Unit-Tests beginnen und dabei Testaufwand einsparen, zumindest bei dem Bemühen, 100 Prozent Code-Coverage für die aufgerufene Unit zu erreichen. Dazu führt man im (nachgelagerten) Unit-Test für die aufgerufene Unit nur noch die Testfälle aus, die benötigt werden, um die Code-Coverage aus dem Integrationstest beider Units auf 100 Prozent anzuheben.

Beispiel

Im Beispiel wird die Programmiersprache C verwendet; damit sind die Units Funktionen im Sinne von C.

Jetzt Newsletter abonnieren

Verpassen Sie nicht unsere besten Inhalte

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung. Die Einwilligungserklärung bezieht sich u. a. auf die Zusendung von redaktionellen Newslettern per E-Mail und auf den Datenabgleich zu Marketingzwecken mit ausgewählten Werbepartnern (z. B. LinkedIn, Google, Meta).

Aufklappen für Details zu Ihrer Einwilligung

Bild 1: Zwei Testfälle führen Teile der aufgerufenen Unit inc_dec() aus.(Bild:  Hitex GmbH)
Bild 1: Zwei Testfälle führen Teile der aufgerufenen Unit inc_dec() aus.
(Bild: Hitex GmbH)

Im Bild 1 sind Code-Überdeckungen grün unterlegt. Ein Testfall für die Unit inc() (links oben) überdeckt die Unit inc() zu 100 Prozent (Mitte) und überdeckt auch das Case-Label „up“ (Zeilen 13 bis 15) in der Switch-Anweisung der aufgerufenen Unit inc_dec() (rechts).

Bild 1 zeigt noch einen zweiten Testfall (links unten), und zwar für die Unit dec(). Dieser Testfall überdeckt das Testobjekt dec() zu 100 Prozent und überdeckt auch das Case-Label „down“ (Zeilen 16 bis 18) in der Switch-Anweisung der aufgerufenen Unit inc_dec(). Das Default-Label (Zeilen 19 und 20) in der Switch-Anweisung der Unit inc_dec() wird von keinem der beiden Testfälle übergedeckt und ist deshalb rot unterlegt. Es kann weder durch einen Aufruf von inc() noch durch einen Aufruf von dec() überdeckt werden. Um das Default-Label zu überdecken, muss man den Unit-Test für die Unit inc_dec() mit einem geeigneten Testfall durchführen.

Bild 2: Ein zusätzlicher Testfall für inc_dec() überdeckt das Label „default“.(Bild:  Hitex GmbH)
Bild 2: Ein zusätzlicher Testfall für inc_dec() überdeckt das Label „default“.
(Bild: Hitex GmbH)

Im Bild 2 ist links der zusätzliche Unit-Testfall für die Unit inc_dec() dargestellt. Er verwendet als Eingabe den unzulässigen Parameterwert 99 für die Unit inc_dec(), bei der nur up und down zulässig sind. Deshalb wird bei seiner Ausführung das Default-Label überdeckt. Damit erreicht inc_dec() zusammen mit den beiden vorherigen Testfällen aus Bild 1 die volle Coverage von 100 Prozent. Dies ist auf der rechten Seite von Bild 2 dargestellt, wo alle Zeilen von inc_dec() grün unterlegt sind.

Zwischenfazit

Es ist also nicht notwendig, drei Unit-Testfälle für inc_dec() auszuführen, um eine 100%ige Überdeckung für inc_dec() zu erreichen, wie es ohne die technische Möglichkeit des Zusammenführens der Coverage der Fall wäre. Im vorliegenden Beispiel werden also zwei Testfälle zur Erreichung von 100 Prozent Coverage eingespart. TESSY [4], das Werkzeug zum automatisierten Modul-/Unit-Test von eingebetteter Software, kann, so wie oben gezeigt, Coverage-Messungen zusammenführen. Bei TESSY heißt diese Fähigkeit „Hyper-Coverage“. Diese Fähigkeit kann nicht nur beim Integrationstest von Units mit Aufrufbeziehungen angewendet werden, sondern auch bei Units ohne Aufrufbeziehung.

Integrationstest mit Units ohne Aufrufbeziehung

Nicht immer stehen Units in einer Aufrufbeziehung. Trotzdem muss ihr Zusammenspiel getestet werden, was im Integrationstest geschieht. Ein Beispiel hierfür ist der abstrakte Datentyp Stack mit seinen Push- und Pop-Operationen. Diese beide Operationen kann man als Software-Units betrachten. Sie stehen nicht in einer Aufrufbeziehung, denn weder ruft push() pop() auf noch umgekehrt. Die beiden Units interagieren über gemeinsame Daten (dem Stack). Interessant ist als Test der Integrationstest, eine Folge von Aufrufen von push() und pop(). Bei TESSY heißt diese Art des Integrationstest „Komponententest“. Eine Komponente umfasst eine oder mehrere Units und die dazugehörigen Daten. TESSY kann auch Coverage aus Komponententest und Unit-Test zusammenführen. Damit ist es technisch möglich, zuerst Coverage im Komponententest zu messen und danach die noch nicht überdeckten Code-Bestandteile im Unit-Test gezielt zu durchlaufen, um insgesamt auf 100 Prozent Coverage zu kommen.

Beispiel

Bild 3: Addition von Coverage aus Komponenten-Test und Unit-Test(Bild:  Hitex GmbH)
Bild 3: Addition von Coverage aus Komponenten-Test und Unit-Test
(Bild: Hitex GmbH)

Bild 3 zeigt das Flussdiagramm (oben) und den Quellcode (unten) der Unit push() eines Stacks. Durchlaufene Bestandteile (Zweige im Flussdiagramm bzw. Zeilen im Quellcode) sind grün markiert; nicht durchlaufene Bestandteile rot. Auf der linken Seite ist das Ergebnis des Komponententests für die Unit push() dargestellt. Offensichtlich ist im Komponententest kein Testfall durch den then-Zweig der if-Anweisung in der Unit push() gelaufen, dem Aufruf der Unit error(), denn dieser Aufruf ist rot unterlegt. Also ist in keinem Komponententestfall ein Stack-Überlauf vorgekommen, denn sonst wäre die Unit error() aufgerufen worden. Hat man das Ziel, 100 Prozent Zweigüberdeckung der Unit push() zu erreichen, kann man im Unit-Test der Unit push() gezielt einen Testfall ausführen, der den then-Zweig der if-Anweisung durchläuft, womit man die Coverage für diesen Zweig erhält. Diese Situation zeigt die Mitte von Bild 3. Durch die Fähigkeit von TESSY, Coverage aus Komponententest und Coverage aus Unit-Test zusammenzuführen, hat man zusammen 100 Prozent Zweigüberdeckung erreicht, und zwar ohne einen Unit-Testfall, der den else-Teil der if-Anweisung durchläuft. Das zeigt die rechte Seite von Bild 3. Man hat also einen Unit-Testfall eingespart.

Was, wenn 100 Prozent nicht erreicht werden können?

Es gibt allerdings Situationen, bei denen auch weitere Testfälle nicht 100 Prozent Code-Coverage erreichen können.

Bild 4: Das Default-Label wird normal nicht erreicht.(Bild:  Hitex GmbH)
Bild 4: Das Default-Label wird normal nicht erreicht.
(Bild: Hitex GmbH)

Im Bild 4 ist eine Situation dargestellt, in der das Default-Label in Zeile 53 bei normaler Ausführung nicht erreicht werden kann; somit wird auch die Unit error() nie aufgerufen. Das liegt an der Anweisung in Zeile 38, nach der die Variable i nur die Werte 0, 1, 2 oder 3 haben kann. Für jeden der vier Werte gibt es ein passendes Case-Label. Deshalb wird der Zweig zum Default-Label nicht durchlaufen und die Code-Coverage erreicht nicht 100 Prozent. Im vorliegenden Fall können 4 von 5 Zweigen ausgeführt werden und die Zweigüberdeckung kann maximal 80 Prozent erreichen. Mit Hinblick auf defensive Programmierung verlangen Kodierstandards die explizite Programmierung eines Default-Labels, beispielsweise in MISRA C:2023 [5] die Regel 16.4. Aber auch wenn das Default-Label nicht explizit vorhanden ist, existiert trotzdem ein zugehöriger Zweig, der nicht ausgeführt werden kann. D.h. selbst wenn die Zeilen 53 bis 55 fehlen würden, kann man normal trotzdem maximal 80 Prozent Zweigüberdeckung erreichen.

Bei sicherheitskritischer Software müssen Situationen, in denen 100 Prozent Code-Coverage nicht erreicht wird, untersucht werden. Helfen zusätzliche Testfälle nicht weiter, so wie im Beispiel in Bild 4, gibt es zur Abhilfe die Möglichkeiten Dokumentation oder Fehlerinjektion.

Dokumentation: Es wird eine Begründung („justification“) dokumentiert, wieso in der spezifischen Situation 100 Prozent Coverage nicht erreicht werden kann und wieso dies in Ordnung ist. Solche Begründungen, gerne mit Namen und Datum versehen, kann man mit Bordmitteln erstellen und verwalten. Komfortable Lösungen mit Bordmitteln erfordern allerdings Aufwand. Vorteilhaft ist es, wenn ein Testwerkzeug hierbei hilft. Bei TESSY kann man eigene Begründungen erstellen oder aus vorgefertigten Begründungen für die häufigsten Situationen auswählen und diese Begründungen Zeilen im Quellcode zuordnen. Die Begründungen erscheinen automatisch im Testreport.

Bild 5: Im Coverage-Review wird begründet, wieso die Zeilen 53 bis 56 nicht ausgeführt werden können.(Bild:  Hitex GmbH)
Bild 5: Im Coverage-Review wird begründet, wieso die Zeilen 53 bis 56 nicht ausgeführt werden können.
(Bild: Hitex GmbH)

Im Bild 5 wird im Werkzeug TESSY dem nicht überdeckten Default-Label (Zeilen 53 - 56) eine der vorgefertigten Begründungen „Unreachable default branch“ zugeordnet. Dies wirkt auf die Hyper-Coverage. Im vorliegenden Fall wird die Hyper-Coverage für die Unit func01() auf 100 Prozent erhöht; die Zweigüberdeckung bleibt bei 80 Prozent, denn der fehlende Zweig wurde ja nicht ausgeführt.

Fehlerinjektion: Das Default-Label wird erreicht (und die Unit error() aufgerufen) wenn der Wert der Variablen i in Bild 4 zwischen der Zuweisung in Zeile 38 und der Verwendung in der switch-Anweisung in Zeile 39 künstlich geändert wird, und zwar gezielt auf einen anderen Wert als 0, 1, 2 oder 3. Das ist eine Fehlerinjektion und bildet einen Hardware-Fehler nach. Technisch erreicht man dies im Unit-Test durch Einfügen von Code direkt nach Zeile 38, in der der Wert von i auf beispielsweise 4 gesetzt wird. Dies kann man mit ein bisschen Bastelei und der Hilfe des Präprozessors mit Bordmitteln erreichen. Diese Einfügung darf natürlich nicht in den Produktions-Code kommen; das wäre fatal! Sie darf auch nur in einem speziellen Testfall aktiv sein und die Ausführung der anderen Testfälle nicht beeinflussen. Es ist sehr vorteilhaft, wenn ein Testwerkzeug dabei hilft.

Das Werkzeug TESSY kann vom Anwender gewünschten Code an ausgewählten Stellen einbauen und so die Fehlerinjektion bei gewissen Testfällen auslösen. Die Gefahr, dass der eingefügte Code im Produktions-Code aktiv bleibt besteht nicht, denn TESSY macht die Einfügung in einer Kopie des zu testenden Quellcodes; der originale Quellcode wird nicht angetastet. Im Fehlerinjektionsfall wird der Code nach dem Default-Label tatsächlich ausgeführt, d.h. man kann sein funktionales Verhalten prüfen und man erhält die Code-Überdeckung für diesen Code. Bei TESSY kann man dadurch auf „echte“ 100 Prozent Zweigüberdeckung kommen; dies ist ein Unterschied zur obigen Lösung mit Code-Review und Dokumentation.

Fazit

Durch die Möglichkeit, Coverage aus verschiedenen Testaktivitäten zusammenzufassen (bei TESSY „Hyper-Coverage“ genannt), kann sich die Zahl der benötigten Testfälle reduzieren, die nötig sind, um auf 100 Prozent Code-Coverage zu kommen. Dies ist besonders nützlich um mit wenig Aufwand herauszufinden, ob es noch nicht getesteten Code gibt.

Literatur- und Quellenverzeichnis

[1] IEC 61508, Functional safety of electrical /electronical/programmable electronic safety related systems, second edition, 2010.

[2] ISO 26262, International Standard, Road vehicles – Functional Safety, second edition, 2018

[3] IEC 62304, Edition 1.1, 2015-06, VDE Verlag GmbH

[4] www.hitex.de/tessy Mehr zum Testwerkzeug TESSY

[5] MISRA C:2023, Guidelines for the use of the C language in critical systems, The MISRA Consortium Limited, UK, Edition 3, Revision 2, April 2023.

 (mbf)

* Frank Büchner hat ein Diplom in Informatik und widmet sich seit vielen Jahren dem Thema Testen und Software-Qualität. Momentan arbeitet er als „Principal Engineer Software Quality“ bei der Fa. Hitex GmbH in Karlsruhe.

(ID:50071850)