Suchen

Schnelles Erstellen von Modultests durch innovative Codeanalyse, Teil 1

| Autor / Redakteur: Miroslaw Zielinski * / Sebastian Gerstl

Modultests sind für Entwickler oft lästige, aber unverzichtbare Pflicht. Moderne Codeanalyse kann diesen Prozess massiv vereinfachen. Teil 1 dieses zweiteiligen Beitrags befasst sich mit dem Vorteil des Einsatzes moderner Codeanalyse-Algorithmen.

Firmen zum Thema

Schneller zu höherer Software-Qualität: Innovative Methoden moderner Codeanalyse können das Erstellen von Modultests deutlich einfacher und schneller gestalten.
Schneller zu höherer Software-Qualität: Innovative Methoden moderner Codeanalyse können das Erstellen von Modultests deutlich einfacher und schneller gestalten.
(Bild: ©DragonImages - stock.adobe.com)

Unter Modultests bzw. Unit-Tests versteht man das Testen einzelner Softwaremodule (Units) oder Anwendungskomponenten, um nachzuweisen, dass diese wie vorgesehen funktionieren. Mehrere Modultestfälle ergeben eine Regressionstest-Suite, die die Korrektheit der Implementierung belegt und ungewollte Änderungen in den Softwaremodulen verhindert. Unit-Tests haben sich als Prüftechnik im Enterprise-IT-Bereich, in Embedded-Applikationen und sicherheitskritischen Anwendungen etabliert.

Auf dem Enterprise-IT-Sektor gelten sie als probates Verfahren und werden häufig im Rahmen der testgetriebenen Entwicklung (Test-Driven Development, TDD) angewandt. Auf dem Gebiet der sicherheitskritischen Anwendungen schreiben viele Normen (etwa ISO 26262, IEC 61508 usw.) Modultests direkt oder indirekt vor. Weil sie aber oft zeitaufwändig sind, suchen Teams nach Möglichkeiten zur Optimierung ihrer Maßnahmen, insbesondere bei sicherheitskritischen Anwendungen, wo der Nachweis der Testabdeckung eine zwingende Voraussetzung für die Zertifizierung ist.

Parasoft, ein Anbieter von automatisierten Werkzeugen für Softwaretests, hat Untersuchungen durchgeführt, wie sich Modultests vereinfachen lassen. Dieser Beitrag erläutert die Ergebnisse in zwei Teilen anhand von Anwendungsfällen: Teil 1 erläutert den Einsatz von modernen Codeanalyse-Algorithmen und wie sie Unit-Test-Erstellung fördern können. Teil 2 (Veröffentlichung Mitte September und ELEKTRONIKPRAXIS 17/2020) behandelt die automatische Erkennung von Eingaben und Reaktionen nachgebildeter Komponenten, die die Codeabdeckung und die automatisierte Generierung von Testfällen maximieren.

Modultests als Methodik bei Entwicklungsprozessen

Modultests als Methodik können in vielen verschiedenen Entwicklungsprozessen genutzt werden, beispielsweise in einem lose definierten, testgetriebenen Entwicklungsprozess (Test-Driven Development, TDD) ohne formelle Anforderungen. Ebenso sind sie im Rahmen eines rigorosen Arbeitsablaufs einsetzbar, um die Konformität zu Functional-Safety-Standards wie ISO 26262 oder IEC 61508 zu erzielen. Oft werden Modultests mit Code-Coverage-Metriken und Reports zur Rückverfolgbarkeit der Anforderungen kombiniert, um Rückmeldungen hinsichtlich der Vollständigkeit des Testprozesses zu erhalten.

Im Idealfall erstellt man Modultests auf manuellem Weg auf der Basis einer Analyse von low-level Softwareanforderungen. Diese Vorgehensweise gibt der ISO 26262 und andere Functional-Safety-Standards vor. Sie findet auch in reinen TDD-Szenarien Gebrauch, wo die Entwickler das gewünschte Verhalten der in der Entwicklung befindlichen Softwaremodule analysieren und die Anforderung in Form des Testfalls festschreiben. In beiden Fällen ist das Erstellen von Modultests ein zeitaufwändiger und teurer, manueller Prozess, bei dem es nicht ohne menschliche Intelligenz geht.

In diesem Zusammenhang sind automatisch erstellte Modultests nur begrenzt in der Lage, sich an den Voraussetzungen zu orientieren. Derzeit kann kein System die in natürlicher Sprache formulierten Anforderungen automatisch analysieren und präzise in Testfälle umsetzen, die sich zum Validieren der Implementierung heranziehen lassen. Allerdings gibt es in realen Entwicklungs-Workflows durchaus einige Fälle, in denen das Erstellen von Modultests durch erweiterte Codeanalyse und partielle Automatisierung unterstützt werden kann.

Die Experimente von Parasoft zielten darauf ab zu ermitteln, wie praxistauglich die Kombination aus erweiterter Codeanalyse und partieller Automatisierung als Unterstützung für das Erstellen von Modultests ist. Sie wurden intern bei Parasoft durchgeführt – ohne die akademische Stringenz und Kontrolle, die normalerweise kennzeichnend für formelle Studien sind. Es ging primär darum, ein besseres Verständnis für den Nutzen und die geschäftliche Einsetzbarkeit erster Versionen der Prototypen zu erhalten. Erläutert werden zudem die gängigsten Anwendungsfälle, in denen die erweiterte Codeanalyse und Automatisierung effektive Unterstützung beim Erstellen von Modultests leisten können.

Hindernisse für Modultests und Ziel-Anwendungsfälle

Zu den größten Hürden für die Einführung von Modultests in einen Softwareentwicklungs-Prozess gehört ihre hohe Arbeitsintensität. Zeitraubend sind vor allem die drei folgenden Bereiche des Modultest-Prozesses:

  • Entwicklung: Erstellen der Testfälle;
  • Abdeckung: Analysieren von Lücken in der Code Coverage, Hinzufügen fehlender Testfälle und Korrektur der Voraussetzungen;
  • Instandhaltung: Pflege und Überarbeitung der Modultestfälle als Reaktion auf Änderungen am getesteten Code.

Zeit- und Ressourcenknappheit schränken das Investment der Teams in Modultests ein. Weil sie keinen hinreichend großen Nutzen aus dieser Praktik ziehen können, geben sie sie wieder auf. Teams, die an der Entwicklung sicherheitskritischer, auf Konformität zu Functional-Safety-Standards abzielende Software arbeiten, haben es auch schwer, genügend Zeit für Modultests freizustellen. Als Folge werden die Tests oft bis in eine späte Phase des Zyklus hinausgeschoben und dienen dann nur dem einen Zweck, die Konformitätsanforderungen zu erfüllen. Dadurch kommen die potenziellen Vorteile von Modultests nur eingeschränkt zum Tragen.

In beiden Fällen könnten sich Verbesserungen für die Nutzung von Modultests und deren Effektivität ergeben, wenn sich der manuelle Arbeitsaufwand mithilfe automatisierter Modultest-Tools verringern ließe.

In den Untersuchungen ging Parasoft von zwei Arten von Personen bzw. Anwendern aus, die von automatisierten Tools zur Unterstützung von Modultest-Prozessen profitieren können:

  • Entwickler in Teams, in denen die Konformität zu Functional-Safety-Standards obligatorisch ist und Modultests zwingend notwendig sind; und
  • Entwickler in Teams, die zwar keine Konformitätsanforderungen erfüllen müssen, denen die Qualität aber so wichtig ist, dass sie dennoch in Modultests investieren.

Für die anvisierten Anwender wurden vier Anwendungsfälle bestimmt, in denen die Automatisierung auf Basis der Codeanalyse einen Nutzen für Entwickler bieten kann:

1. Unterstützung bei der automatischen Generierung eines Mindestumfangs an Tests, um eine hohe Codeabdeckung zu erzielen;

2. Hilfestellung beim Verstehen und Analysieren von Lücken in der Codeabdeckung sowie beim automatischen Synthetisieren des Testfalls auf Basis des Vektors;

3. Support bei der Generierung von Äquivalenzklassen für die Funktions- bzw. Methodenparameter, um das manuelle Erstellen von Testfällen zu verbessern. Äquivalenzklassen werden auf der Basis des Quellcodes von Funktionen bzw. Methoden generiert, nicht aber auf der Grundlage der Anforderungen (so genanntes „Gray-Box Testing“);

4. Unterstützung bei der Feststellung von Testfällen mit dem gleichen Abdeckungs-Footprint („doppelte“ Testfälle), um potenzielle Redundanzen in der Regressionstest-Suite zu entfernen.

Der folgende Abschnitt konzentriert sich speziell auf den ersten Anwendungsfall. In einem nachfolgenden Beitrag gehen wir näher auf den zweiten Anwendungsfall ein und erläutern, wie Entwickler automatisierte Impulse für die Testfall-Generierung zur Analyse von Abdeckungslücken nutzen können.

Ein Mindestumfang an Tests für hohe Codeabdeckung

Für diese Anwendung sollte ein minimaler Umfang an Testfällen erstellt werden, um eine schnelle Steigerung der Abdeckung zu fördern.

Wenn ein Anwender Testfälle für die Codedurchdringung automatisch erstellen möchte, wird vorausgesetzt, dass das Validieren von speziellen Anforderungen nicht an erster Stelle steht. Ziel ist dagegen meistens, möglichst viele Codezeilen auszuführen und zu beobachten, wie die Software auf die Eingaben, die vom Tool zur automatischen Modultest-Generierung erzeugt wurden, reagiert.

Das Wort „zufällig“ wird hier bewusst nicht verwendet. Die für die automatische Testgenerierung genutzten Algorithmen sollen für das zu testende Modul einen optimalen Satz Eingangsvektoren errechnen, der die Codeabdeckung maximiert. Hier liegt der Fokus auf der Zeilen-, Anweisungs- oder Verzweigungs-Abdeckung, allerdings können vor allem im Zusammenhang mit Funktional-Safety-Standards auch anspruchsvollere Metriken wie etwa MC/DC zum Einsatz kommen.

Behauptungen (Assertions) werden bei der automatischen Generierung von Testfällen entweder nicht erzeugt oder anhand der Reaktion des geprüften Moduls automatisch aufgezeichnet. Die Frage nach dem Nutzen dieser Arten von Testfällen ist in der Industrie durchaus strittig. Das grundlegende Argument gegen auf diese Weise erzeugten Assertions ist ihr mangelnder Nutzen für das Validieren der Anforderungen.

Vorteile automatisch generierter Modultests

Automatisch generierte Testfälle helfen bei so genannten Robustheits-Tests, beim „Error Guessing“ (Erraten von Fehlern) und (in gewissem Grad) bei der Fehlereinstreuung (Fault Injection) [4]. Alle diese Methoden werden von Functional-Safety-Standards wie ISO 26262, DO-178C oder IEC 61508 empfohlen [1, 2, 3].

Außerdem können ausschließlich zum Validieren der zentralen Anforderungen erstellte Testfälle reale Probleme, die sich mit automatisch generierten Tests offenlegen lassen, übersehen. Ebenso ist es möglich, dass die Teilemengen von Werten für Funktions- und Methodenparameter, die für das Validieren von Anforderungen verwendet werden, nicht dafür geeignet sind, die wirklichen Probleme im Code aufzudecken. Automatisch generierte Tests, die möglichst viel Code durchdringen sollen, haben dagegen das Potenzial zum Aufdecken vieler Problemarten, wie zum Beispiel:

  • Nullzeiger-Dereferenzierungen,
  • Divisionen durch null,
  • Stack-Überläufe,
  • nicht abgefangene Ausnahmen,
  • Überläufe (Integer/Gleitkomma), oder
  • Gleichungen

Durch die allgegenwärtige Vernetzung sowie wachsende Sicherheitsbedenken haben Tests mit automatisch generierten Testfällen an Bedeutung gewonnen. Der Grund dafür ist, dass alle soeben genannten Softwarefehler die Applikation zum Abstürzen bringen und ernste Sicherheitslücken hervorrufen können. Die Fähigkeit zum Auffinden potenzieller Programmabstürze ist von entscheidendem Wert für die Bemühungen, das System abzusichern und den Code stabiler und robuster zu machen.

In einem sicherheitsorientierten Szenario ist es ein Vorteil, dass die zur Stimulierung der Software verwendeten Parameter mit Konstanten und Literalen korrespondieren, die in der analysierten Software gefunden werden, denn man simuliert hierdurch eine der für Angriffe genutzten Techniken. Dies steht im Gegensatz zu aus Anforderungen abgeleiteten Parametern, die von einem Analysten oder einer für das Formulieren von Softwareanforderungen zuständigen Person geschrieben wurden.

Methodik zur Generierung der Testfälle

In der ersten Phase des Experiments mit automatisch erstellten Testfällen wurden die folgenden Algorithmen zur automatischen Generierung von Eingabewerten für die geprüften Funktionen bzw. Methoden untersucht:

  • Grenzwerte für die Parameter auf Basis der Datentyp-Information aus dem Syntaxbaum, und
  • Extrahieren und Mutieren von Literalen und Konstanten aus dem Quellcode für deren Einsatz als Eingangswerte.

Durchgeführt wurde das Experiment mit Parasoft C/C++test, das automatisch Modultestfälle auf Basis des Quellcodes generieren kann.

In der zweiten Phase kam eine zusätzliche Strategie ins Spiel: Der Einsatz eines „Wörterbuchs“ von Parameterwerten in Form der „Factory-Methoden“, die repräsentative Instanzen der komplexen Typ-Parameter produzieren.

In Phase drei erfolgte die Verbindung der statischen Analyse-Engine von Parasoft C/C++test sowie der Daten- und Kontrollfluss-Analyse mit dem Testfall-Generator. Das Ziel war, die Eingabewerte zum Ausführen jener Zeilen, die nach der ersten und zweiten Phase immer noch zur Ausführung anstanden, bereitzustellen. Die Tester kombinierten hier die Ausgabe der Ablaufanalyse-Engine mit dem symbolischen Ausführungsmodul zum Lösen von Gleichungen und Ungleichungen, welche das Ablaufanalyse-Modul aus den Entscheidungspunkten im geprüften Code extrahierte.

Ergebnisse aus der Ausführung automatisch generierter Fälle

Die Codeabdeckungs-Ergebnisse aus der Ausführung der automatisch generierten Testfälle werden für jede Phase separat ausgegeben. Die nachstehenden Tabellen enthalten auch Angaben zur Anzahl der beim Ausführen der Testfälle aufgetretenen Abstürze, wodurch das System sicherheitsmäßigen Risiken ausgesetzt wird. Nicht berücksichtigt in der Spalte „Anzahl der Abstürze“ sind diejenigen Abstürze, die nach der ersten Bereinigung nicht mehr auftraten. Ein Bereinigungsschritt soll das Rauschen („Noise“) eliminieren, das durch nicht oder falsch initialisierte globale Variablen hervorgerufen wird.

Die in der ersten Phase erzielten Ergebnisse (Tabelle 1) lassen den Schluss zu, dass es auch mit relativ einfachen Strategien, die auf Grenzfällen von Datentypen und der Mutation von im geprüften Code vorgefundenen Konstanten und Literalen basieren, möglich ist, eine beträchtliche Codeabdeckung zu erzielen und Codekonstrukte aufzudecken, die nicht vor unerwarteten Eingaben geschützt sind.

Projekt Erzielte Zeilenabdeckung Anzahl der Abstürze
(Potenzielle Probleme)
P7zip9.2 43% 1252
fftw3 61% 355

In der zweiten Phase (siehe Tabelle 2) kam ein Verzeichnis von Parametern in Form von Factory-Funktionen, die Instanzen von komplexen Typen produzieren, zum Einsatz. Die Tatsache, dass nur eine relativ geringfügige Verbesserung festbestellt wurde, begründet sich daraus, dass man während des Experiments nur begrenzte Zeit in das Aufbereiten und Abstimmen der Verzeichnisse investierte. Zwar ist Parasoft grundsätzlich der Auffassung, dass diese Taktik sehr nützlich sein kann, jedoch muss vor dem Starten des Modultest-Generators zunächst Zeit investiert werden. Hier braucht es zweifellos weitere Forschung.

Projekt Erzielte Zeilenabdeckung Anzahl der Abstürze
(Potenzielle Probleme)
P7zip9.2 47% 1104
fftw3 63% 327

Die dritte Phase des Experiments (siehe Tabelle 3) zeigt eine deutliche Verbesserung in punkto Codeabdeckung durch die Ausführung automatisch generierter Testfälle und die während der Ausführung aufgetretenen Abstürze. Von ungefähr 30% der zufällig ausgewählten Abstürze stellte sich bei 15% heraus, dass sie ein potenzielles Risiko für das System darstellten.

Projekt Erzielte Zeilenabdeckung Anzahl der Abstürze
(Potenzielle Probleme)
P7zip9.2 61% 1756
fftw3 82% 403

Ausblick: Automatisierte Testfälle zur Coverage-Analyse

Im ersten Teil ging es um den Einsatz von modernen Codeanalyse-Algorithmen und wie sie den Vorgang der Modultesterstellung fördern. Im nachfolgenden Teil zwei geht es um die automatische Erkennung von Eingaben und Reaktionen nachgebildeter Komponenten, die die Codeabdeckung und die automatisierte Generierung von Testfällen maximieren.

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

* Miroslaw Zielinski ist Product Manager bei der Parasoft Corporation.

(ID:46672955)