Erstellen von Modultests durch Codeanalyse, Teil 2: Analyse von Abdeckungslücken

Von Miroslaw Zielinski * |

Anbieter zum Thema

Teil zwei der Reihe zu einfacheren Modultests: Wie lässt sich durch die Analyse von Reaktionen nachgebildeter Komponenten die Codeabdeck- ung und die automatisierte Generierung von Testfällen maximieren?

Analyse von Code: 
Durch die Autamatische Erkennung von Eingaben lässt sich schnell eine möglichst vollständige Codeabdeckung bei Modultests erreichen.
Analyse von Code: 
Durch die Autamatische Erkennung von Eingaben lässt sich schnell eine möglichst vollständige Codeabdeckung bei Modultests erreichen.
(Bild: gemeinfrei / Pixabay)

Im ersten Teil dieser zweiteiligen Reihe zur einfacheren Generierung von Modultests (erschienen in ELEKTRONIKPRAXIS-Ausgabe 15/2020 und online) ging es um den Einsatz von modernen Codeanalyse-Algorithmen und wie sie den Vorgang der Modultesterstellung fördern. Im nachfolgenden Teil zwei betrachten wir einen zweiten Anwendungsfall näher. Hierbei geht es um die automatische Erkennung von Eingaben und Reaktionen nachgebildeter Komponenten, die die Codeabdeckung und die automatisierte Generierung von Testfällen maximieren. Für diesen Anwendungsfall wurden Parameterwerte und andere Software-Impulse automatisch berechnet mit dem Ziel, die Analyse von Abdeckungslücken aufzuwerten und fehlende Testfälle zu generieren.

Die zum Validieren der grundlegenden Software-Anforderungen erstellten Testfälle liefern die Bestätigung, dass die gesamte erforderliche Funktionalität tatsächlich implementiert ist, und dass die Softwaremodule wie vorgesehen reagieren. Gewöhnlich wird die Anforderungs-Testabdeckung mit der Codeabdeckung kombiniert, um Unzulänglichkeiten im Testprozess aufzudecken.

Die Norm ISO 26262 beschreibt die empfohlene Vorgehensweise in Teil 6, Unterabschnitt 9.4.4: „Wird die erreichte strukturelle Abdeckung als unzureichend erachtet, sind entweder zusätzliche Testfälle zu generieren, oder es ist eine Argumentation auf der Basis der anderen Methoden vorzulegen.“ In den Beispielen zu diesem Abschnitt heißt es weiter: „Die Analyse der strukturellen Abdeckung kann Unzulänglichkeiten in den anforderungsbasierten Testfällen und den Anforderungen sowie toten Code, deaktivierten Code oder ungewollte Funktionalität aufdecken.“ Der Analysevorgang von Abdeckungslücken ist zeitaufwändig und setzt ein Verständnis des Verarbeitungsablaufs des Codes voraus, einschließlich zwischenzeitlicher Parameterwerte und solider Kenntnisse der Anforderungen an das jeweilige Softwaremodul.

In Projekten, für die die Konformität zu Functional-Safety-Standards zwingend vorgeschrieben ist, setzt man die Analyse von Codeabdeckungslücken ein, um fehlende oder unkorrekt formulierte Anforderungen oder aber Probleme mit der Umsetzung von Anforderungen festzustellen. Den Abschluss der Analyse bilden Korrekturen an der Anforderungsbasis oder im Quellcode. Abdeckungslücken sind nur dann akzeptabel, wenn dafür eine hinreichende Begründung geliefert werden kann, sh. Norm ISO 26262, Teil 6, Unterabschnitt 9.4.4.

Für nicht-sicherheitskritische Projekte werden Lücken in der Codeabdeckung analysiert, um zu bestimmen, welches Maß an geschäftlichen Risiken sich aus dem nicht getesteten Code ergibt.

Methodik: So wurde die Analyse durchgeführt

Das Experiment basierte auf der Annahme, dass sich die Produktivität der Entwickler durch Hinweise darauf steigern lässt, welche Parameter erforderlich sind, um bestimmte Zweige des Codes auszuführen. Diese Hinweise wurden auf der Basis des berechneten Vektors automatisch generiert.

Für das Experiment realisierte Parasoft eine Erweiterung für das Code-Coverage-Modul von Parasoft C/C++test. Mit dieser als „Coverage Assistent“ bezeichneten Erweiterung können Entwickler den nicht abgedeckten Codeabschnitt direkt im Code-Editor markieren und einen oder mehrere Testvektoren generieren, mit dem bzw. denen sich die Software so stimulieren lässt, dass sie die markierten Zeilen ausführt. Testvektoren werden für jene Funktion bzw. Methode erstellt, die ungeprüfte Codezeilen enthält. Die Testvektoren bestehen aus folgenden Elementen:

  • Parameterwerte für Funktionen bzw. Methoden;
  • Werte für die globalen Variablen (wenn diese in dem Verarbeitungspfad durch die markierte Zeile vorkommen); und
  • Rückgabewerte zum Initialisieren von Stubs bzw. Mocks externer Funktionen, die Bestandteile des Verarbeitungspfads durch die markierte Zeile sind.

Im Experiment ging man davon aus, dass die einzelnen Module (Funktionen/Methoden) isoliert geprüft werden, das bedeutet: Alle aus der getesteten Funktion heraus aufgerufenen Funktionen/Methoden werden durch Stubs ersetzt, die sich so programmieren lassen, dass sie die erforderlichen Werte zurückgeben.

Die Zeilen 32 bis 34 des Beispiels in Bild 1 wurden von den bestehenden Tests nicht abgedeckt. Der Anwender kann nun von der Erweiterung den Testvektor zum Ausführen von Zeile 32 und weiterer Zeilen im Block anfordern. Als Resultat erhält er den vom System ausgegebenen Testvektor (Bild 2).

Die vom Coverage Assistant ausgegebenen Testvektoren, die die Software stimulieren und damit die markierten Codezeilen ausführen, gehören derselben Äquivalenzklasse an, weshalb nur eine Instanz des Testvektors dargestellt wird. Anhand dieser Information kann ein Entwickler die Parameter mit den elementaren Softwareanforderungen vergleichen und analysieren, weshalb der jeweilige Abschnitt nicht von den anforderungsbasierten Testfällen abgedeckt wurde.

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.

Aufklappen für Details zu Ihrer Einwilligung

Im nächsten Schritt des Experiments wurde der Coverage Assistant um Funktionalität erweitert, die es ihm ermöglicht, auf der Basis des von der Erweiterung berechneten Testvektors automatisch einen Modultestfall zu synthetisieren. Mit dieser Funktionalität gerüstet, könnten Entwickler, die an einem standardkonformen Projekt arbeiten, die folgende Vorgehensweise übernehmen:

1. Mit einer interaktiven Abdeckungs-Ansicht und Reportmechanismen wird eine Abdeckungslücke entdeckt.

2. Vom Coverage Assistant wird ein Testvektor zum Ausführen der nicht abgedeckten Zeile angefordert.

3. Der vom Coverage Assistant automatisch generierte Testvektor wird analysiert und anhand der grundlegenden Anforderungen des betreffenden Moduls geprüft.

4. Wird im Zuge dieser Analyse ein fehlender Testfall aufgedeckt, wird der Testfall für den Testvektor generiert und im Interesse der Rückverfolgbarkeit mit der Anforderungs-ID versehen.

5. Sollte die Analyse Mängel in der Definition der Anforderungen ergeben, werden Abhilfemaßnahmen im RMS (Regelwerk) ergriffen, an die sich Aktionen ab Punkt 4 anschließen können.

Als nächstes verglich man die Codeabdeckung zweier unabhängiger Entwicklerteams, die aus je drei Personen bestanden und über vier Tage hinweg an derselben Codebasis gearbeitet hatten. Gegenstand des Experiments waren ein Codemodul mit 120.000 Codezeilen sowie Modultests, die etwa 55% der Codezeilen abdeckten. Die erste Gruppe nutzte bei den Modultests und der statischen Analyse eine reguläre Version von Parasoft C/C++test, während der zweiten Gruppe die durch den Coverage Assistant erweiterte Version zur Verfügung stand. Beide Teams hatten die Aufgabe, die von den existierenden Testfällen nicht abgedeckten Codezeilen zu analysieren und eine der folgenden Abhilfemaßnahmen zu ergreifen:

  • Hinzufügen eines fehlenden Testfalls;
  • Identifizieren einer fehlenden Anforderung und des Testfalls (anstatt einer vollständigen Definition der fehlenden Anforderung sah das Experiment nur das Erstellen eines Tickets mit einer Beschreibung dessen, was fehlt, vor);
  • Korrektur der Anforderung (anstatt einer Korrektur der Anforderung sah das Experiment lediglich das Erstellen eines Tickets mit einer Beschreibung dessen, was korrigiert werden sollte, vor); und
  • Qualifikation als toter Code.
Erzielte Zeilen-Abdeckung Zahl der generierten neuen Testfälle Zahl der erstellten Tickets zur Verbesserung der Anforderungsbasis
Gruppe 1 (Basis-Funktionalität) 67% 380 23
Gruppe 2 (mit Coverage Assistant) 76% 730 36

Konkrete Ergebnisse aus der Analyse

Der Vergleich der Ergebnisse erwies sich allerdings als schwierig: Die Teams hatten die Freiheit, mit ihrer Arbeit an beliebigen Zeilen zu beginnen. Möglicherweise konnte sich ein Team also für einen relativ einfachen Teil des Projekts entscheiden. Zudem wurde die Qualität der zur Verbesserung der An­forderungsbasis erstellten Tickets nicht bewertet. Vielmehr ging man davon aus, dass die Entwickler hinreichende Analysen vorgenommen hatten, um dem Analysten genügend Informationen zur Korrektur der Anforderung an die Hand zu geben. Möglicherweise gibt es hier jedoch Unterschiede.

Auch arbeiteten die Teams nicht kontinuierlich an dem Projekt. Stattdessen wurde das Experiment als zusätzliche Aufgabe durchgeführt, verteilt auf Zeitabschnitte über einen Zeitraum von drei Wochen, sodass jedes Teammitglied kumuliert vier Tage damit beschäftigt war. Es ist möglich, dass die Mitglieder eines Teams von einem geringeren Druck durch ihre normale Tätigkeit und von weniger Kontextwechseln profitierten.

Auch bei Berücksichtigung der genannten Faktoren scheinen die Ergebnisse auf ermutigende Weise die Annahme zu stützen, dass sich die Produktivität verbesserte.

Ein interessantes und bemerkenswertes Resultat ist, wie schnell die neue Funktionalität Akzeptanz fand. Die Entwickler der zweiten Gruppe waren anfangs noch nicht davon überzeugt, ob die automatisch generierten Hinweise für die manuelle Testfall-Generierung möglich ist und die Produktivität verbessern kann. Das im Anschluss an das Experiment geführte Interview ergab eine hohe Wertschätzung seitens der Entwickler für die Möglichkeit zum automatischen Erstellen eines Testfalls auf Basis des berechneten Vektors, da sich der Aufwand an mühsamer manueller Arbeit hierdurch stark reduzierte.

Mit den Untersuchungen wollte Parasoft Innovationen identifizieren, die geeignet sind, um die Produktivität von Entwicklern beim Erstellen von Modultests zu verbessern. Im Fokus standen dabei zwei bestimmte Anwendungsfälle, die man zu Beginn der Studien festgelegt hatte:

1. Unterstützung der Entwickler bei der automatischen Generierung eines Mindestumfangs an Testvektoren für eine hohe Codeabdeckung.

2. Support der Entwickler beim Verstehen und Analysieren von Lücken in der Codeabdeckung und bei der automatischen Synthese von Testfällen auf der Basis des Vektors.

Bessere Produktivität beim Erstellen von Modultests

Zur besseren Beurteilung des wirklichen Nutzens für die Endanwender erstellte Parasoft einen Prototyp eines automatisierten Tools, das ausgewählte Anwendungsfälle unterstützt. Für den Prototyp wurde das statische Analysemodul Parasoft C/C++test mit dem Modultest- und dem Codeabdeckungs-Modul verbunden. Parasoft C/C++test dient zum Auffinden von Bugs und zum Überwachen der Konformität zu Standards wie MISRA, AUTOSAR oder CERT. Die Informationen über statisch berechnete Ausführungspfade und Parameter fanden Berücksichtigung beim Generieren von Testvektoren und Testfällen. Die Prototypen wurden bei Parasoft intern von verschiedenen Entwicklerteams an ausgewählten Open-Source-Projekten und Produktionscode eingesetzt.

In der Industrie gibt es den populären Trend, Sicherheitslücken mittels automatisierter Fuzz-Tests aufzudecken. Hier passt die automatische Generierung von Testfällen zum Erzielen einer hohen Abdeckung (Steigerung der Codedurchdringung) ausgezeichnet dazu. Diese Testfälle sind nicht als Grundlage für Regressionstests zu verwenden. Vielmehr sollten Unternehmen, ob sie nun in sicherheitskritischen Bereichen tätig sind oder nicht, dieses Konzept als zusätzliche Möglichkeit zum Erkennen von Sicherheitslücken und als Bestandteil von Robustheits- oder Error-Guessing-Tests betrachten.

Auch wenn hier noch mehr Forschung notwendig ist, scheint es ein überaus effektiver Ansatz zu sein – insbesondere in einem vollautomatischen Konzept mit einer leistungsfähigen Flow Analysis Engine und symbolischen Solvern, die eine tiefe Durchdringung des Codes garantieren können.

Der zweite als „Coverage Assistant“ bezeichnete Prototyp wurde implementiert, um den Zeitaufwand zum Auffinden von Abdeckungslücken zu veringern. Damit verfolgte man das Ziel, automatisch errechnete Hinweise für Entwickler zu liefern, die verstehen wollen, weshalb die vorhandenen Testfälle einen bestimmten Teil des Quellcodes nicht abdecken. Die Hypothese war dabei, dass ein Entwickler das Problem einer unzureichenden Codeabdeckung einfacher untersuchen kann, wenn ihm bestimmte Werte für Funktionsparameter und globale Variablen sowie die erwarteten Rückgabewerte abhängiger Funktionen zur Verfügung gestellt werden. Wenn die Analyse in einem späteren Abschnitt der Modultest-Phase nachweist, dass ein Testfall fehlt, lässt sich dieser automatisch synthetisieren.

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

In dem eingeschränkten Experiment registrierte man eine deutliche Produktivitätssteigerung des Teams, das die erweiterte Funktionalität nutzte. Nach Berichten des Teams erzielte die Fähigkeit zur automatischen Testfall-Generierung eine große Zeitersparnis und reduzierte die Frustrationen im Zusammenhang mit mühevoller Handarbeit.

Ungeachtet der Einschränkungen dieser Untersuchung zieht Parasoft den Schluss, dass der Produktivitätsgewinn hoch genug ist, um mit der Produktisierung der Proto­typen fortzufahren.

* Miroslaw Zielinski ist Product Manager bei der Parasoft Corporation.

(ID:46752305)