Fuzzy Testing zur Generierung von Eingabedaten für Testfälle
Auch in gut getesteter Software treten regelmäßig Fehler erst im produktiven Betrieb auf, da sich bestimmte Benutzeraktionen und -eingaben nur schwer vor hersehen lassen. Hier sollen maschinengenerierte Zufallsdaten helfen.
![Fuzzy Testing zur Generierung von Eingabedaten für Testfälle](https://heise.cloudimg.io/width/610/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/1/9/1/1/1/2/5/testing-b5e001571cc56dc8.jpeg)
- Maximilian Kögel
- Jonas Helming
- Julian Sommerfeldt
Der Begriff Fuzzy Testing oder Fuzzing stammt ursprünglich aus dem Bereich der Sicherheits- und Penetrationstests. Das Ziel ist, ein System mit zufälligen, mitunter unzulässigen Anfragen auf unbekannte Schwachstellen zu überprüfen. Das Konzept der zufälligen Testfälle lässt sich aber nicht nur für Sicherheitstests nutzen.
Der Artikel ist ein Erfahrungsbericht über den Einsatz von Fuzzy Testing zur Generierung von Eingabedaten für Testfälle. Die technische Umsetzung des konkreten Beispiels basiert auf Java, Eclipse und dem Eclipse Modeling Framework (EMF) und steht als Open-Source-Framework zur Verfügung. Das verwendete Konzept und die gesammelten Erfahrungen lassen sich jedoch auf andere Bereiche übertragen.
Eingabedaten randomisieren
In vielen Bereichen lässt sich durch Whitebox-Testing, die Bildung von Äquivalenzklassen oder das gezielte Überprüfen von Randfällen eine gute Testabdeckung erreichen. Es gibt aber immer wieder Fälle, in denen beinahe beliebig viele mögliche Eingabedaten für einen bestimmten Testfall zu prüfen wären. Solche Fälle betreffen meist generische Features, die auf beliebigen Eingabedaten funktionieren müssen. Ein gutes Beispiel dafür ist eine Import/Export-Funktion. Um wirklich sicher zu gehen, dass Import und Export gut funktionieren, müsste man beide Features mit beliebigen Eingabedaten durchspielen. Ein solches Vorgehen ist in der Praxis sicherlich nicht durchführbar. Wünschenswert wäre es aber, die Funktion mit möglichst vielen Eingabedaten zu testen und dabei kritische Wertekombinationen zu entdecken. Da eine manuelle Erstellung der Daten kostspielig ist, ist dieser Schritt, falls möglich, zu automatisieren. Dazu sind zwei Voraussetzungen essenziell: eine generische Assertion und eine ausreichende formale Beschreibung der Eingabedaten.
Eine generische Zusicherung für einen Testfall bedeutet, dass die Korrektheit eines Testlaufs ohne vorherige Kenntnis der konkreten Eingabedaten überprüfbar sein muss. Im Beispiel des Import/Export-Features lässt sich ein solcher Testfall erzeugen, indem man Daten aus einer Datei zunächst importiert und anschließend exportiert. Das Ergebnis lässt sich nun mit der Ursprungsdatei vergleichen. Ist der Inhalt beider Dateien identisch, hat der Import/Export für die getesteten Eingabedaten funktioniert. Ob die Anwendung die Daten zwischen Import und Export korrekt verarbeitet, lässt sich auf diese Weise nicht überprüfen.
Eine formale Beschreibung der Eingabedaten definiert, welche Eingaben überhaupt möglich sind. Liegt die Beschreibung formal vor, beispielsweise als XML Schema (XSD), lassen sich auf der Basis valide Eingabedaten automatisch generieren. Für die praktische Durchführung eines solchen Testvorgehens sind einige Komponenten notwendig. Zum einen benötigt man einen Datengenerator, der auf Basis einer Beschreibung der Datenstruktur, etwa XSD, UML oder EMF Ecore, Dateninstanzen generiert.
Weiterhin wird häufig zusätzlich ein Datenmutator benötigt, um die Dateninstanzen während eines Tests pseudozufällig zu modifizieren. Für spezifische Asserts sind Hilfsfunktionen wie ein generischer Klon und der Vergleich von Dateninstanzen hilfreich.
Im Weiteren beschreiben die Autoren die genannten Komponenten allgemein und konkretisieren diese am Beispiel von EMF. Im folgenden Kasten finden sich Informationen, um den Sourcecode für eine Fuzzy-Testing-Implementierung für EMF aus dem Projekt EMFStore herunterzuladen. Auf die Implementierung beziehen sich auch die Beispiele.
Fuzzy-Testing für EMF
Zum Selbertesten braucht man den Sourcecode aus den Repositories der Website www.eclipse.org/emfstore/getting involved.php und hier die Plug-ins der folgenden Repositories:
- org.eclipse.emf.emfstore.core.git:
- org.eclipse.emf.emfstore.modelmutator
- org.eclipse.emf.emfstore.fuzzy
- org.eclipse.emf.emfstore.fuzzy.emf
- org.eclipse.emf.emfstore.fuzzy.emf.edit
- org.eclipse.emf.emfstore.fuzzy.emf.editor
- org.eclipse.emf.emfstore.fuzzy.emf.example (einfache Beispiele)
- org.eclipse.emf.emfstore.fuzzy.emf.test (komplexere Beispiele)
Spielerei mit den Daten
Der Datengenerator soll auf Basis eines Eingabewerts, genannt Seed, eine gültige Dateninstanz im Sinn der formalen Datenstrukturbeschreibung liefern. Dabei ist es essenziell, dass er deterministisch arbeitet. Das heißt, der Generator liefert für den gleichen Seed immer identische Dateninstanzen. Ohne diese Eigenschaft lassen sich die Testergebnisse später nicht wiederholen, was für die Behebung von Fehlern unverzichtbar ist. Handelt es sich bei den Daten beispielsweise um die Beschreibung von Bauteilen sowie den Eigenschaften und Beziehungen der Bauteile, gilt es, mit einem bestimmten Eingabewert genau eine Bauteilsammlung zu erstellen. Zusätzlich werden typischerweise zur Generierung weitere Konfigurationsdaten benötigt.
Es ist insbesondere sinnvoll, die Größe der zu generierenden Daten zu definieren. Außerdem ist es oft wichtig, Einschränkungen der zu erzeugenden Daten vornehmen zu können, etwa zum Ausschluss bestimmter Datentypen. Für EMF existiert eine Implementierung eines solchen Generators. Sie erlaubt es, Instanzen eines Modells mit EMF Ecore zu generieren. EMF erlaubt, auf Basis des EMOF-Standards (Essential Meta Object Facility) die Struktur von Daten in einer UML-ähnlichen Sprache zu definieren. Außer einem Seed und einer gewünschten Anzahl an Objekten übergeben Entwickler dem Generator ein Root Object, das als Start für die Generierung dient. Zusätzlich werden Packages konfiguriert, die die Klassen der zu generierenden Objekte einschränken.
Der Datenmutator erlaubt es wiederum, mit dem Seed Dateninstanzen zu beeinflussen. Die Konfiguration ist im Wesentlichen ähnlich zum Generierungsfall, zusätzlich lässt sich aber die Menge und Art der vorzunehmenden Änderungen bestimmen. Der Datenmutator kann Testfälle erzeugen, die das korrekte Verhalten der zu testenden Software im Falle von Datenänderungen überprüfen. Nach jeder Mutation wird der Assert des Testfalls mit den neuen Eingabedaten wiederholt.
Das folgende Codebeispiel füllt ein gegebenes Root Object (Root des Containment-Baums der Modellinstanz) mit 1.000.000 zufälligen Instanzen aus einem angegebenen Package. Der Generator füllt dabei Attributwerte der generierten Objekte mit zufälligen Werten. Zusätzlich werden randomisiert Referenzen zwischen Objekten erzeugt. Es entsteht also eine gefüllte und vernetzte Beispielinstanz. Die Zufälligkeit bezieht sich auf den gewählten Seed, sodass die Ergebnisse reproduzierbar sind.
ModelMutatorConfiguration config
= new ModelMutatorConfiguration(ePackage, rootEObject, 1);
config.setMinObjectsCount(1000000);
ModelMutator.generateModel(config);