Testgenerierung in Java - eine Übersicht
So langsam möchte ich meine Erkenntnisse über Testgenerierung zusammenfassen. Seit etwa einem Jahr habe ich einen Vortrag über Testgenerierung in Java auf diversen Konferenzen positioniert und inzwischen gibt es so etwas wie eine Essenz aus diesen Vorträgen …
Der Zweck von Testgenerierung
Wenn ich Software schreibe, dann schreibe ich Tests! Idealerweise vor dem Code, öfter mal aber auch mit dem Code und selten sogar nach Auslieferung des Codes. Aber ich nutze keine Testegeneratoren. Warum?
Aus meiner Sicht sind Testgeneratoren kein Ersatz für sorgfältig geschriebene Unit-Tests, Komponenten-Tests oder Integrations-Tests. Es gibt überhaupt nur wenige Situationen wo generierter Testcode hilfreich ist.
Die Situation, die mir für Testgeneratoren geeignet scheint ist wie folgt charakterisiert:
- Große, gewachsene Codebasis
- Nichttriviale Komplexität
- Geringe Testabdeckung
- Neue Anforderungen
- Zeitdruck
Dieses Szenario ist also das klassische Legacy-Code-Szenario. Die Erweiterung von Legacy Code setzt voraus, dass man den Code Refactorn kann (um letztlich geeignete Stellen für das neue Feature zu finden). Refactorn setzt voraus, dass Tests existieren. Und Zeitdruck bedeutet, dass wir beides nur unzureichend können.
Testgenerierung hat somit die Rolle des Schopfs an dem man sich sprichwörtich aus dem Sumpf ziehen kann. Voraussetzung ist natürlich, dass die Testgenerierung einigemaßen gründlich ist.
Testgenenerierung und Programmanalyse - Dynamisch, Statisch, Ohne?
Ganz grob kann man beim Thema Testgenerierung drei verschiedene Ansätze unterscheiden:
- Ohne Programmanalyse: Diese Ansätze haben die wenigsten Voraussetzungen, man benötigt lediglich ein Programm bei dem die Typen und Referenzen aufgelöst sind. Typischerweise wird mit heuristischen und syntaktischen Mitteln versucht eine Testüberdeckung zu erreichen. Der Vorteil ist, dass die Theorie für diese Ansätze sehr überschaubar ist und man die Cleverness über andere Techniken (z.B. künstliche Intelligenz) einbringen kann, die besser skalieren. Der Nachteil ist, dass man auf essentielle Informationen bei der Generierung verzichten muss.
- Statissche Programmanalyse: Diese Ansätze analysieren den Daten- und Kontrollfluss des zu testenden Programms. Statische Analyse hat einige Vorteile, z.B. kann sie vollständig ohne User-Interaktion ablaufen, hat aber sehr wohl analytische Informationen über das Programm zum Generierungszeitpunkt. Der Nachteil ist, dass manche Probleme der statischen Analyse unlösbar sind und manche Lösungen zu generisch sind, um einen praktischen Nutzen zu haben.
- Dynamische Programmanalyse: Dieser Ansatz analysiert nicht den möglichen Daten- und Kontrollfluss, sondern den faktischen. Dynamische Programmanalyse erfolgt zur Laufzeit und ist damit immer sehr nahe am wirklichen Nutzungsprofil des Programms. Der Nachteil ist, dass dynamische Programmanalyse keine allgemeinen Aussagen über Programme machen kann, sondern nur Aussagen über das beobachtete Verhalten. Außerdem erfordert dynamische Programmanalyse eine realistische Interaktion mit dem Programm, d.h. sorgfältige User-Interaktion.
Eine gute Übersicht über einzelne Ansätze - wissenschaftlich zusammengefasst - findet findet man im Paper An Orchestrated Survey on Automated Software Test Case Generation (diverse Autoren, leider nichts über dynamische Analyse). Eine gepflegte Liste von Werkzeugen zu dem Thema findet man unter Code-based test generation (Zoltán Micskei). Die folgende Bewertung kann dazu genutzt werden einen detailierteren Überblick über die Tool-Landschaft zu bekommen:
Tools ohne Programmanalyse
Die zwei populärsten Open-Source-Testgenerierungs-Werkzeuge sind Werkzeuge, die ohne Programmanalyse funktionieren.
Randoop erzeugt eine große Menge von Sequenzen, die aus semantisch korrekten Operationen auf einer API basieren. Für jede Sequenz werden alle literalwertigen Zwischenergebnisse (d.h. primitive Typen oder Strings) aufgezeichnet. Im Test werden genau diese Sequenzen wieder abgespielt und es wird verifiziert, dass die Zwischenergebnisse identisch zu den aufgenommenen sind. Zustandsänderungen durch Methoden wird nicht direkt wahrgenommen (da eben keine Analyse stattfindet), vielmehr verlässt man sich hier darauf, dass Änderungen sich in veränderten Zwischenergebnissen reflektieren (was zumindest in der Theorie und bei sehr großer Menge von Sequenzen stimmen muss). Randoop ist ein stabiles Programm, welches stets kompilierbaren Code generiert. Die Qualität der generierten Tests ist leider eher schlecht. Viele wissenschaftliche Studien vergleichen neue Ansätze mit Randoop, vermutlich weil Randoop so stabil ist (die meisten wissenschaftlichen Ansätze sind das nicht) und weil der einfach Ansatz einfach zu schlagen ist.
Der Quellcode ist gut strukturiert, die API ist sichtbar und verwendbar. Die Unit-Tests sind von eher unterdurchschnittlicher Qualität, zahlreiche Tests sind auskommentiert, andere enthalten unnötig viel Programmlogik und was überhaupt getestet wird, wird auch eher selten klar.
Evosuite basiert ebenfalls auf der Generierung zufälliger Statements. Wie Randoop zeichnet es literalwertige Zwischenergebnisse auf. Die Sequenzen werden aber nachträglich noch einmal gefiltert und optimiert - um die Redundanz und die schlechter Lesbarkeit zu reduzieren. Dies erfolgt durch genetische Algorithmen, die eine Menge an Ausgangssequenzen auf Qualitätsmetriken (z.b. auf hohe Coverage) optimieren und kürzen. In der Konsequenz sind generierte Tests deutlich besser als eine rein zufällige Aufrufsequenz. Im Gegensatz zu vielen anderen Forschungsprojekten ist Evosuite stabil, dokumentiert und fertig für den produktiven Einsatz. Von allen Tools aus der Forschung ist evosuite zu Recht das populärste. Meiner Einschätzung nach hat Evosuite als Tool allerdings schon seine Grenzen erreicht.
Der Quellcode offenbart eine sehr komplexe Architektur. Leider handelt es sich um ein integriertes System. Eine brauchbare public API in Java gibt es nicht. Die Qualität des Quellcodes ist eher mäßig. Es gibt eher wenige Unit-Tests, und viele haben eher den Character von Golden-Master-Tests (fixieren den Output zu gegebenem Input).
Tools mit statischer Programmanalyse
Die verbreiteten Ansätze, um mit statischer Programmanalyse Tests zu generieren, sind das Symbolic Execution und Concolic Execution (Concrete & Symbolic Execution). Die Forschung auf diesem Gebiet ist sehr intensiv. Leider sind die Ergebnisse eher ernüchternd.
Symbolic Pathfinder basiert auf der offenen JVM Java Pathfinder. Es erweitert den Java Pathfinder um Symbolic Execution. Die Dokumentation, wie das erreicht werden soll, ist empfehlenswert und gut verständlich. Die Umsetzung ist in einem sehr frühen Experimentalstadium stecken geblieben und seitdem vermutlich auch nicht wieder ernsthaft aufgenommen worden. Die Installation ist kompliziert und viele der Beispiele laufen nicht ohne weiteres zutun. Meiner Einschätzung nach sind viele Beispiele mit dem aktuellen Codestand sogar gar nicht mehr lauffähig.
Der Quellcode enthält sehr viele Kommentare und macht auch sonst den Eindruck, dass er nicht fertig entwickelt wurde. Auch handwerklich ist der Code eher nicht solide. An Tests gibt es überwiegend Programme, deren Output nicht selbst-validierend ist, d.h. der Tester muss anhand des Ergebnisses selbst entscheiden ob der Test erfolgreich war. Ich denke, dass bei dieser Code-Qualität langfristig kaum noch Fortschritte zu erwarten sind.
Starfinder basiert ebenfalls auf der offenen JVM Java Pathfinder. Starfinder ist ein neuer Ansatz um Java Pathfinder mit Symbolic Execution auszustatten. Leider ist auch dieses Projekt nicht mehr als ein Forschungsprototyp.
Der Quellcode wirkt nicht so unruhig wie der von Symbolic Pathfinder, man findet wenige verstörende Kommentare und auch wenig auskommentierten Code. Leider findet man auch wenige Tests, die das ordentliche Verhalten einzelner Methoden testen. Automatisierte Tests auf Unit-Level gibt es fast keine, die überwiegende Masse von Tests haben Golden-Master-Test-Character. Immerhin wird bei vielen Tests klar was sie gerade Testen. Ich vermute allerdings trotzdem, dass dieses Projekt eher nicht als Basis für weitere Entwicklung verwendet wird.
CATG/Janala2 ist ein Werkzeug für Concolic Testing von Java Code. Die Dokumentation des Tools ist derart rudimentär, dass allein schon der Zweck nicht so einfach zu erfahren ist (vermutlich war er in den Forschungspapers dazu besser beschrieben). So wie es aussieht, geht es darum Eingabedaten für Tests zu generieren. Die Installation ist sehr schwer und weicht inzwischen auch stark von der Dokumentation ab (falsche Versionen, falsche Artefaktnamen, …). Die Brauchbarkeit der Ausgaben ist eher gering.
Der Quellcode wirkt vergleichweise geordnet, Tests gibt es aber praktisch keine. Die Tatsache, dass der Code inzwischen mehrere Jahre alt is ohne Aktualisierungen erfahren zu haben, deutet darauf hin, dass auch niemand mehr daran arbeitet.
KeY ist eigentlich eine JVM für Programmverifikation. Als Nebenprodukt der Forschung kam die Idee auf, dass man aus Spezifikationen auch Tests generieren könnte. Die Installation von KeY zum Zweck der Testgenerieren ist nicht einfach, die Dokumentation ist an vielen Stellen irreführend oder unvollständig. Testgenerierung funktioniert nur, wenn man au dem gleichen System einen Z3-Solver installiert hat. Die volle Funktionalität hat das System nur mit Java 1.4 (keine Generics). Leider sind die erzeugten Tests bei komplexeren Programmen oft nicht kompilierbar und selbst nach manueller Nacharbeit nur wenig plausibel.
Der Quellcode ist gut organisiert und die Codequalität ist recht gut. Auffällig ist, dass es vergleichsweise viele Unit-Tests und Komponenten-Tests gibt (die bei vergleichbaren Projekten oft ganz fehlen). Eher unschön ist hingegen, dass einige der genutzten Bibliotheken (z.b. die Parser die das System auf Java 1.4 limitieren) nur als jar-file vorliegen und somit extrem schlecht anpassbar sind.
Weiterhin erwähnenswert sind die Werkzeuge JPET, JTest und AgitarOne, die in Demonstrationen recht gut aussehen. Da sie aber nicht frei verfügbar sind (JPET ist nur noch im Binärcode für veraltete Plattformen verfügbar, die anderen beiden sind proprietär), war eine Evaluation bisher nicht möglich.
Tools mit dynamischer Programmanalyse
Der vorherrschende Ansatz bei dynamischer Analyse ist der Capture-Replay-Ansatz, der darauf basiert dass der Zustand vor und nach dem Aufruf einer Methoden aufgezeichnet wird und dann als Regressionstest weiterverwendet werden kann.
Testrecorder ist ein Werkzeug das auf dem Capture-Replay-Ansatz basiert. Methoden, die man aufzeichnen möchte, annotiert man mit @Recorded
und führt die Anwendung dann mit einem Agent aus. Das Werkzeug ist modular aufgebaut, man kann die Aufzeichnung, die Generierung und den Bibliotheken für den generierten Code auch unabhängig einsetzen. Da viele Situationen bei der Testgenerierung nicht vorhersehbar sind, ist das Werkzeug auch so designed, dass man es mit eigenen Klassen ergänzen kann, wenn die Standardmechanismen zum Aufzeichnen oder zur Generierung nicht ausreichend sind.
Der Quellcode ist komplex aber clean (keine Kommentare, 90% Testüberdeckung mit Unit-Tests und Integrationstests), die API ist nur rudimentär dokumentiert, da noch nicht ganz klar ist welche Aspekte tatsächlich zur öffentlichen API gehören.
Ein früherer Ansätze zu diesem Thema waren z.B. ThOR was für eingeschränkte Szenarien (Java Beans) brauchbar war, leider aber lange nicht mehr geupdated wurde.