Devmate: Generierung von JUnit- Java- JKU ...
←
→
Transkription von Seiteninhalten
Wenn Ihr Browser die Seite nicht korrekt rendert, bitte, lesen Sie den Inhalt der Seite unten
Eingereicht von Jakob Faschinger Angefertigt am Institute for System Software Betreuer o.Univ.-Prof. Dr. Hanspeter Mössenböck Devmate: April 2021 Generierung von JUnit- Testfällen aus Java- Methodensignaturen Masterarbeit zur Erlangung des akademischen Grades Diplom-Ingenieur im Masterstudium Computer Science JOHANNES KEPLER UNIVERSITÄT LINZ Altenbergerstraße 69 4040 Linz, Österreich www.jku.at DVR 0093696
Masterarbeit o.Univ.-Prof. Dr. Hanspeter Mössenböck devmate: Generierung von JUnit-Testfällen aus Java- Methodensig- Institute for System Software naturen T +43 732 2468 4340 F +43 732 2468 4345 Student: Jakob Faschinger hanspeter.moessenboeck@jku.at Betreuer: Prof. Hanspeter Mössenböck Secretary: Birgit Kranzl Beginn: 1. Juni 2020 Ext 4341 birgit.kranzl@jku.at devmate ist ein Werkzeug für Softwareentwickler, mit den man schnell und einfach Unit-Tests generieren kann. Um dieses Ziel zu erreichen, werden Testentwicklungs- verfahren wie Äquivalenzklassenanalyse oder Randwertanalyse verwendet. Das Testen einer Methode in devmate besteht aus folgenden Schritten: Anhand einer beste- henden Methodensignatur wird ein Skelett eines Testmodells erstellt. Dieses Modell kann der Tester um Äquivalenzklassen, Repräsentanten und Testfälle erweitern. Das erweiterte Test- modell kann in eine Unit-Test-Klasse überführt werden. Der beschriebene Prozess ist unabhängig von Programmiersprachen und Unit-Test-Frame- works. Jedoch muss devmate für jede Programmiersprache und für jedes Unit-Test-Frame- work erweitert werden. devmate unterstützt aktuell C# als Programmiersprache und NUnit als Unit-Test-Framework. Eine Erweiterung von devmate um Java und JUnit ist Ziel dieser Arbeit. devmate besteht aus folgenden 4 Komponenten: • IDE+Parser. Diese Komponente ist in eine IDE integriert, extrahiert aus Methodensignatu- ren Informationen und übersetzt diese in ein sprachunabhängiges Modell (Public Language, oder kurz PL). Zusätzlich übernimmt diese Komponente die Koordination zwischen den Ser- ver-Komponenten. • Test Generator. Hier sind die Testentwurfsverfahren (z. B. die Äquivalenzklassenanalyse) implementiert. Diese Komponente ist sprachunabhängig. • Code-Generator. Diese Komponente generiert Test-Code für ein Unit-Test-Framework. • Code-Merge. Ist für die Vereinigung verschiedener Test-Code Dateien verantwortlich. Daraus ergeben sich die inhaltlichen Schwerpunkte dieser Arbeit: • Entwicklung eines Plugins für eine Java-IDE (TBD) im Rahmen des devmate Produkts, wel- ches Informationen aus Methodensignaturen extrahieren kann. • Entwicklung eines Generators, der aus dem devmate Testmodell JUnit-Code erstellt. Die Arbeit ist in regelmäßigen Abständen mit dem Betreuer sowie mit der Kontaktperson des Unternehmens zu besprechen. Ein Zeitplan mit Milestones ist innerhalb von 3 Wochen nach Beginn der Arbeit abzuliefern. Der Zeitplan soll im Laufe der Arbeit verfeinert und aktualisiert werden, um sicherzustellen, dass die Arbeit zeitgerecht fertiggestellt wird. Die endgültige Fassung der Masterarbeit soll nicht später als 31. Mai 2021 abge- JOHANNES KEPLER UNIVERSITÄT LINZ geben werden. Altenberger Straße 69 4040 Linz, Österreich www.jku.at DVR 0093696
Eidesstattliche Erklärung Ich erkläre an Eides statt, dass ich die vorliegende Masterarbeit selbstständig und ohne fremde Hilfe verfasst, andere als die angegebenen Quellen und Hilfsmittel nicht benutzt bzw. die wörtlich oder sinngemäß entnommenen Stellen als solche kenntlich gemacht habe. Die vorliegende Masterarbeit ist mit dem elektronisch übermittelten Textdokument identisch. Linz, April 2021 Unterschrift i
Danksagungen Ich möchte meinem Betreuer, Herrn o.Univ.-Prof.Dr. Hanspeter Mössenböck, danken, dass ich die Möglichkeit hatte, diese Arbeit bei ihm zu schreiben. Besonderes schätzte ich die schnelle Beantwortung von E-Mails und die Möglichkeit, recht spontane Meetings über Zoom abzuhalten. Dank gilt natürlich auch der Firma “Automated Software Testing” und insbesondere Johannes Bergsmann und David Thiel, da ich dort 7 Monate arbeiten durfte und sie mir in zahlreichen Meetings geholfen haben, meinen Code zu schreiben. Besonderer Dank geht auch noch an Johannes Hochrainer, der eng mit mir zusammengearbeitet hat und mir bei meinen zahlreichen Fragen stets zur Seite stand. Schlussendlich möchte ich mich auch noch bei meiner Familie bedanken, welche mich mein ganzes Studium unterstützte. Vor allem auch bei meiner Frau Rebekka, die sich etliche Vorträge anhören musste und den Text korrekturgelesen hat, obwohl sie meis- tens nichts verstehen konnte. Zusätzlich war sie noch die gesamte Dauer meiner Arbeit schwanger, schaffte es aber trotzdem, mir immer den Rücken freizuhalten, sodass ich die Möglichkeit hatte, zügig voranzukommen. Als Letztes möchte ich auch noch meiner Tochter Amelie danken, da sie mir durch ihre bevorstehende Geburt, die notwendige Motivation gab, auch wirklich fertig zu werden. ii
Kurzfassung Da Software Projekte immer größer werden, wird es stets schwieriger sie zu testen und da- mit auch zunehmend teurer, da mehr Entwicklungszeit investiert werden muss. Devmate ist ein Werkzeug, welches Entwicklern hilft, Unit Tests zu erstellen, indem Methoden- signaturen analysiert und mithilfe von Äquivalenzklassen automatische Testfälle erstellt werden. Obwohl Devmate zum Teil sprachneutral aufgebaut ist, unterstützte es zunächst nur C#. Das Ziel dieser Arbeit war, es um die Funktionalität für Java zu erweitern. Da- für wurden zwei Plug-ins für Eclipse und IntelliJ geschrieben, sowie ein Code-Generator, welcher Test-Klassen für JUnit 5 erstellt. Abstract As software projects tend to grow, it is more and more difficult to test them adequately. This also makes development more expensive, as time to test can’t be used to develop new software. Devmate is a tool which helps developers with creating Unit tests from method signatures. It uses equivalence classes to automatically generate test cases. Devmate is partly language independent, but initially only supported C#. The goal of this thesis was to extend Devmate, so that it supports also Java. For that, two plug-ins were developed, one for Eclipse and one for IntelliJ. In addition to that, a Code Generator, which creates JUnit 5 classes was written. 1
Inhaltsverzeichnis Kurzfassung 1 Abstract 1 1 Einführung 5 2 Hintergrund 7 2.1 Testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.1 White-Box-Testen . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.2 Black-Box-Testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.2 Xtend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3 Devmate 11 3.1 Benutzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.2 Architektur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 4 Implementierung 23 4.1 Model-Builder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 4.1.1 Eclipse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 4.1.2 IntelliJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 4.1.3 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 4.2 Test-Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 4.3 Code-Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 4.4 Testmethodik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 5 Weiterführende Arbeiten 55 5.1 CodeMerge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 5.2 Java Plug-ins für weitere IDEs . . . . . . . . . . . . . . . . . . . . . . . . 55 5.3 Weitere Test-Frameworks für den Code-Generator . . . . . . . . . . . . . . 55 5.4 Erweiterung des Eclipse Plug-ins um auch C/C++ zu unterstützen . . . . 56 6 Zusammenfassung 58 7 Verzeichnisse 60 3
1 Einführung Da Software heute immer größer wird und immer mehr Codezeilen enthält, ist es auch immer schwieriger zu überprüfen, ob der Code auch tatsächlich das macht, was er soll. Code musste schon immer getestet werden, aber mit wachsender Größe wird auch diese Aufgabe immer komplexer. Es ist nicht zumutbar, nach jeder Änderung alle Use-Cases manuell zu überprüfen, da dies viel zu viel Zeit in Anspruch nimmt. Deswegen werden Tests geschrieben, welche automatisch überprüfen, ob der Code den Anforderungen ent- spricht und keine Fehler enthält. Aber auch diese Tests zu schreiben ist aufwendig und es können leicht Anwendungs- und Spezialfälle übersehen werden. Aus diesem Grund sind Werkzeuge hilfreich, welche einen Teil dieser mühseligen Arbeit abnehmen, indem sie automatisiert Testfälle erstellen. Damit können sich die Entwickler mehr auf die we- sentlichen Dinge, wie das Schreiben des Codes, konzentrieren. Devmate [2] ist solch ein Werkzeug. Es soll Entwicklern helfen, Black-Box-Testfälle für Methoden zu generieren (siehe Abschnitt 2.1.2). Dafür muss der Benutzer nur verschie- dene Äquivalenzklassen für die Parameter der Methode definieren und Repräsentanten für diese festlegen (siehe Abschnitt 2.1.2). Devmate kann dann automatisch Testfälle er- stellen und eine gültige Testklasse generieren. Vor diesem Projekt unterstützte Devmate nur die Programmiersprache C# [13]. Obwohl C# viele Entwickler benutzen, gibt es andere Sprachen, welche häufiger benutzt werden [21]. Um mehr potenzielle Kunden zu gewinnen und Devmate vielseitiger zu machen, war das Ziel dieser Arbeit, Devmate um die Sprache Java [18] zu erweitern. Diese ist, laut Oracle, die Nummer 1 der Programmiersprachen und wird von 12 Millionen Entwicklern und 5 Millionen Studenten benutzt [19]. Devmate selbst wurde auch großteils in Java geschrieben. Dies stellt eine weitere Bereicherung dar, da die Entwickler, Devmate nun auch mit ihrem eigenen Werkzeug testen können. Im Zuge dieser Arbeit wurden folgende Funktionen implementiert: 1. Zwei Model-Builder, jeweils einer für IntelliJ [10] und Eclipse [6]. Diese bestehen aus einem Parser und einem Mapper, welche den bestehenden AST (Abstract Syn- tax Tree) auf den von Devmate verwendeten GAST (Generic AST) abbilden (Ab- schnitt 4.1). 2. Einen Code-Generator, welcher aus den in Devmate definierten Testfällen, JUnit- Testfälle generiert (Abschnitt 4.3). 3. Eine Verbindung der neu geschrieben Teile, mit dem sprachunabhängigen Test- Generator (Abschnitt 4.2). 5
2 Hintergrund In diesem Kapitel werden einige Begriffe zum Thema Testen erklärt. Zusätzlich wird XTend vorgestellt, da es für den Code-Generator verwendet wurde (siehe Abschnitt 4.3). 2.1 Testen Beim Testen wird ein System, mit dem Ziel Fehler zu finden, ausgeführt [15]. Es gibt verschiedene Methoden, um ein System zu testen. Welche man verwendet, hängt vor allem davon ab, was genau getestet werden soll, welche Fehler man finden möchte und welche Rolle man in dem Projekt einnimmt. Im Gegensatz zu statischen Testtechniken, welche Programmierstil und Komplexität beurteilen ohne den Code ausführen zu müssen, betrachten dynamische Testtechniken die Interaktion mit dem zu testenden Objekt. Im Folgenden werden zwei gegensätzliche Testmethoden betrachtet: 2.1.1 White-Box-Testen Beim White-Box-Testen ist der Quellcode des zu testenden Systems bekannt. Dadurch ist es am besten geeignet, wenn auch die Entwickler selbst testen, da sie den Quellcode schon kennen und dieser nicht weitergegeben werden muss. Getestet wird vor allem die Struktur und welche Pfade im Programm genommen werden. Ziel ist es, eine möglichst große Codeabdeckung zu erreichen, indem versucht wird jeden möglichen Pfad zumindest einmal auszuführen. Abdeckungsarten sind Anweisungsabdeckung, Zweigabdeckung und Pfadabdeckung [20]. 2.1.2 Black-Box-Testen Im Gegensatz zu White-Box-Tests liegt bei Black-Box-Tests der Quellcode des zu testen- den Systems nicht vor. Es sind nur die Ein- und Ausgangsparameter bekannt, während die interne Struktur versteckt ist. Getestet wird, ob die Ausgangsparameter, bei bestimmten Werten der Eingangsparameter, den Wert haben welcher durch die Spezifikationen defi- niert ist. Daher muss der Tester selbst kein Entwickler sein und kann auch ohne Zugriff auf den Quellcode testen. Es reicht, die Schnittstelle des Testobjekts zu kennen [20]. Äquivalenzklassen Da es oft undenkbar ist, alle Werte der Eingangsparameter zu testen, werden Methoden verwendet, um mit wenigen Testfällen möglichst viele Eingabevarianten abzudecken. Die 7
Idee dahinter ist, dass es Äquivalenzklassen gibt, für welche alle Werte dieser Klasse dasselbe Ergebnis liefern [15]. Mit folgendem Beispiel wird dies näher erklärt: In Österreich brauchen Kinder bis zu einem Alter von 14 Jahren einen Kindersitz [16]. Als Äquivalenzklassen haben wir nun für das Alter den Bereich 0-14 und größer 15. Zusätzlich existiert noch der Fall der ungültigen Eingabe mit dem Alter kleiner 0. Es reichen daher, zum Beispiel die Werte 7 und 25 zu testen. Es gibt aber zusätzlich die Regel, dass Kinder ab einer Körpergröße von 1,35 m, unabhängig von ihrem Alter, keinen Kindersitz mehr brauchen. Daher gibt es hier zusätzliche Äquivalenzklassen, welche aber auch abhängig von den vorherigen sind. Wie man in Tabelle 1 sieht, würde man mit der Kombination dieser beiden Parameter, sechs Äquivalenzklassen bekommen, da bei ungültigen Parameterwerten der Wert des jeweils anderen Parameters unerheblich ist. Äquivalenzklasse Alter (Jahre) Größe (cm) Benötigt Kindersitz Repräsentanten X1 0-14 0-134 Ja 7 95 X2 0-14 135+ Nein 7 150 X3 15+ 0-134 Nein 25 95 X4 15+ 135+ Nein 25 150 X5
Parameter G1 G2 Alter (Jahre) -1 0 1 13 14 15 Größe (cm) -1 0 1 134 135 136 Tabelle 2: Repräsentanten für die Grenzwerte G1=0 und G2=14 für Alter und G2=135 für Größe 2.2 Xtend Xtend ist ein Java-Dialekt, welcher nach Java 8 kompiliert und daher ohne Probleme gemeinsam mit Java in einem Projekt verwendet werden kann [8]. Xtend wurde von der Eclipse Foundation als Erweiterung für Eclipse entwickelt. Der Hauptvorteil von Xtend ist die Möglichkeit der Template-Expressions [9]. Diese erlauben Strings lesbar aneinanderzureihen ohne die umständliche Java-Syntax verwenden zu müssen. Text kann einfach so hingeschrieben werden, wie man ihn in der Ausgabe sehen will. Dies inkludiert Zeilenumbrüche und Einrückungen. Code kann im Text verwendet werden, indem er innerhalb “«” und “»” zu finden ist. Dies hilft, den Code lesbar zu halten. Im Vergleich zwischen Java und Xtend in Listing 1 und Abbildung 1 ist zu sehen, wieviel einfacher Xtend die Lesbarkeit des Codes macht. 1 pulbic static String printClass(String className, ClassCode classCode) { 2 StringBuilder sb = new Stringbuilder(); 3 sb.append("@DisplayName(\"Testing Method ").append(className).append("\")").append (System.lineSeparator()); 4 sb.append("public class ").append(StringUtils.capitalize(className)).append("Test {").append(System.lineSeparator()); 5 sb.append("\t").append(printClassCode(classCode)).append(System.lineSeparator()); 6 sb.append("}").append(System.lineSeparator()); 7 return sb.toString(); 8 } Listing 1: Beispiel Java Code Abbildung 1: Der selbe Code in Xtend 9
3 Devmate Devmate ist ein Werkzeug, welches von der “Automated Software Testing GmbH” ent- wickelt und vertrieben wird [2]. Automated Software Testing ist ein hoch innovatives Start-up aus Linz. Die Hauptaufgabe ist, Benutzern zu helfen, schneller und effektiver Unit-Tests zu schreiben, indem einige Schritte automatisiert werden, wodurch schnell viele Testfälle geschrieben werden können. Es wird das Verfahren des Black-Box-Testens (siehe Abschnitt 2.1.2) sowie die Äquivalenzklassenmethode (siehe Abschnitt 2.1.2) ver- wendet, um alle Kombinationen der Parameterwerte einer Methode miteinander zu ver- binden. Vor dieser Arbeit existierte nur ein Plug-in für Microsoft Visual Studio und die Programmiersprache C#. Außerdem wurden bereits die Test-Frameworks NUnit [5], xU- nit [1] und MSTests [14] implementiert. Für die folgende Erklärung wurde dieses Plug-in [3] und das Test-Framework NUnit verwendet [5]. 3.1 Benutzung Auf der Homepage von Devmate ist der Prozess folgendermaßen beschrieben [4]: 1. Ein Modell des Codes erstellen. 2. Äquivalenzklassen und ihre Repräsentanten erstellen. 3. Die Repräsentanten in Testfälle kombinieren. 4. Die Rückgabewerte definieren. 5. Eine Test-Klasse generieren. 6. Optional: Existierenden und neuen Code verknüpfen. Die meisten dieser Schritte sind automatisiert und werden durch Klicken der entspre- chenden Knöpfe ausgelöst. Nur in den Schritten 2 und 4 müssen vom Benutzer selber Daten eingegeben werden. Schritt 3 kann auch vom Benutzer übernommen werden, zu- sätzlich oder als Ersatz der automatisch generierten Testfälle. Im Folgenden werden die einzelnen Schritte näher beschrieben. Als Beispiel wird dabei die in Listing 2 zu sehende Methode verwendet. Listing 2: Methodensignatur für das Beispiel 1 public static double AvgSpeed(DateTime start, DateTime end, double distance) 11
Abbildung 2: Mit einem Klick wird das Modell erstellt 1. Modell erstellen. Wenn der Cursor sich in einer Methode befindet und das Kon- textmenü aufgerufen wird, kann dort die Aktion “Test with devmate” ausgeführt werden (siehe Abbildung 2). Damit wird Devmate ein Modell erstellen, um den Test-Generator starten zu können. Dort kann nun der Benutzer Äquivalenzklassen und ihre Repräsentanten erstellen. 2. Äquivalenzklassen und ihre Repräsentanten erstellen. Zuerst müssen Äqui- valenzklassen gebildet werden (siehe Abbildung 3). Mithilfe von Konstruktoren (sie- he Abbildung 4) und Factory-Methoden (siehe Abbildung 5) können dann dafür Repräsentanten definiert werden. Factory-Methoden erlauben es, eigene Konstruk- toren für komplexe Typen zu definieren. 3. Die Repräsentanten in Testfälle kombinieren. Testfälle können entweder au- tomatisch generiert oder vom Benutzer selbst erstellt werden. Dafür wird der ent- sprechende Knopf gedrückt (siehe Abbildung 6). Die erstellten Tests sind in Abbil- dung 7 zu sehen. 4. Rückgabewerte definieren. Der Benutzer muss hier die zu erwartenden Rückga- bewerte definieren, beziehungsweise angeben, welche Exceptions geworfen werden sollen (siehe Abbildung 7). 5. Eine Test-Klasse generieren. Mit Klick auf den entsprechenden Button (siehe 12
Abbildung 3: Äquivalenzklassen mit Repräsentanten Abbildung 4: Mithilfe von Konstruktoren können Repräsentanten definiert werden. 13
Abbildung 5: Erstellte Factory-Methode mit 3 Parametern: day, hour, minute Abbildung 6: Tests automatisch generieren, selber erstellen oder löschen Abbildung 7: Testfälle mit Rückgabewerte Abbildung 8: Test-Klasse generieren 14
Abbildung 9: Abfrage ob eine neue Test-Klasse erstellt werden soll, oder die alte mit der neuen zusammengefügt werden soll. Abbildung 8) wird die Test-Klasse erstellt. Es wird hier auch angezeigt, ob und wo der erstellte Code gespeichert wurde. Das Zahnrad rechts neben dem Knopf erlaubt es, das Test-Framework zu wechseln. Die erstellte Test-Klasse hat keine Abhängigkeit von Devmate und kann auch ohne weiterverwendet werden. Daher jeder Code der generiert wurde, ist wirklich vom Benutzer für immer verwendbar, ohne dass das Werkzeug weiter verwendet werden muss. Die generierte Test-Klasse ist in Listing 3 zu sehen. 6. Optional: Existierenden und neuen Code verknüpfen. Wenn die Test-Klasse nicht das erste Mal erstellt wurde, wird gefragt ob die alte Version überschrieben werden soll, die neue als eigene Datei gespeichert werden soll oder beide zu einer Datei zusammengefügt werden sollen (siehe Abbildung 9). Da der Benutzer die Test-Klasse in der Zwischenzeit bearbeiten konnte oder eventuell sogar bearbeiten musste (um Factory-Methoden zu implementieren) ist es hilfreich, wenn beim er- neuten Generieren der Test-Klasse, dieser selbst geschriebene Code, nicht wieder gelöscht wird. 15
Listing 3: Generierte Test-Klasse 1 namespace AvgSpeedTestTests 2 { 3 using System; 4 using System.Collections.Generic; 5 using NUnit.Framework; 6 7 public class AvgSpeedTestCase 8 { 9 private static IEnumerable PositiveTests() 10 { 11 yield return new TestCaseData(new DateTime(2021, 1, 20, 15, 0, 0), new DateTime (2021, 1, 20, 16, 0, 0), 100, 100) 12 .SetName("p1") 13 .SetDescription(""); 14 yield return new TestCaseData(new DateTime(2021, 1, 20, 15, 0, 0), new DateTime (2021, 1, 20, 16, 0, 0), 0, 0) 15 .SetName("p2") 16 .SetDescription(""); 17 } 18 19 private static IEnumerable NegativeTests() 20 { 21 yield return new TestCaseData(new DateTime(2021, 1, 20, 15, 0, 0), new DateTime (2021, 1, 19, 15, 0, 0), 0, -1) 22 .SetName("n2") 23 .SetDescription("end: invalid"); 24 yield return new TestCaseData(new DateTime(2021, 1, 20, 15, 0, 0), new DateTime (2021, 1, 20, 16, 0, 0), -10, -1) 25 .SetName("n3") 26 .SetDescription("distance:
45 [Test] 46 [TestCaseSource("TestsThrowingException")] 47 public void AvgSpeedTestThrowingException(DateTime start, DateTime end, Double distance, Type expected) 48 { 49 Assert.Throws(expected, () => CSharpTutorials.Program.AvgSpeed(start, end, distance)); 50 } 51 } 52 } 3.2 Architektur Da der Test-Generator IDE- und sprachunabhängig ist, benötigt er eine Schnittstelle, die mit allen Eventualitäten der jeweiligen Sprachen umgehen kann. Dafür wurde ein Modell verwendet, welches an das Modell des Abstract Syntax Trees (AST) der Object Managment Group (OMG) [17] angelehnt ist. Es wurden dabei aber Änderungen vorge- nommen, um es besser auf die Bedürfnisse von Devmate zuzuschneiden. Deshalb wurden einige Klassen durch Attribute ersetzt und alle Typen erhielten den Suffix “Type”. Dieser AST wird "generic AST (GAST)"genannt. Jedes Objekt der Klasse Type (siehe Abbildung 10) hat mehrere Attribute: • fullName: Der voll qualifizierte Name des Typs. Bei primitiven Typen, ist er identisch mit dem Namen. Da dieser eine eindeutige ID ist, muss immer nur der Name gespeichert werden, um den passenden Typ zu verlinken. • name: Der Name des Typs. • astTypeName: Die Bezeichnung des Typs in diesem Diagramm (daher: “Integer- Type”, “ClassType” und weitere). Dadurch wird in jeder Sprache, der entsprechende Typ übernommen. • nullable: Boolean, der anzeigt ob der Wert des Typs “null” sein darf. • throwable: Boolean, der anzeigt, dass es sich hier um eine Exception handelt. Es werden nun einzelne Typen näher erklärt und auf die Verwendung für Java einge- gangen, bei welchen dies nicht offensichtlich ist. • EnumType: Beinhaltet eine Liste an möglicher Literalen. Ein Literal ist ein String. Eine Instanz dieses Typs, ist einer der hier definieren Literalstrings. • NumberType: In Java sind alle Zahlentypen immer mit einem Vorzeichen verse- hen. 17
18 Abbildung 10: Übersicht über Type und alle Abhängigkeiten
• RealType: Fließkommazahlen mit dem Attribut “precision”. Dieses enthält die Anzahl der Bits, welche nach dem Komma gespeichert sind. Eine äquivalente Klasse zu LongDouble existiert in Java nicht. • IntegralType: Ganzzahlen mit dem Attribut “size”. Dieses beschreibt die Anzahl der Bits, welche für diesen Typ benötigt werden. • StringType: Ist hier ein eigener Typ, anstatt wie in Java einfach eine Klasse zu sein. Dadurch wird kein Konstruktor gebraucht, sondern Strings können einfach eingetippt werden. • ConstructedType: Typen die aus einem Zusammenschluss mehrerer Typen be- stehen. “elementType” ist dabei der voll qualifizierte Name des Typs des Elements. Beim Parsen muss auch der Element-Typ geparst werden. In Java wird dies vor allem für Arrays benötigt. Generische Typen wurden in dieser Arbeit noch nicht implementiert. • ClassType: In objektorientierten Sprachen, wie zum Beispiel Java, sind Klassen ein sehr wichtiger Typ. Die Liste der Konstruktoren erlaubt es, im Test-Generator Instanzen davon anzulegen. Dafür müssen auch jeweils alle Parameter der Kon- struktoren geparst werden, damit deren Typen bekannt sind und die Konstruktoren verwendet werden können. Diese Typen werden an die eigentliche Schnittstelle, die Klasse TestItem, übergeben und dort als Liste gespeichert. 1. TestItem (siehe Abbildung 11) beschreibt eine zu testende Methode: • id: Ein String im UUID Format, welcher für dieses TestItem global eindeutig ist. • name: Der Name der zu testenden Methode. • uri: Der Speicherort der Klasse, welche die Methode enthält. • isStatic: Definiert ob es sich um eine statische oder nicht statische Methode handelt. • inputs: Für jeden Parameter wird ein Port angelegt und in dieser Liste gespei- chert. Zusätzlich wird als erster Parameter die Instanz der Klasse gespeichert, wenn die Methode nicht statisch ist. • outputs: Für den Rückgabewert wird auch ein Port angelegt. 19
Abbildung 11: TestItem • tests: Eine Liste jener Tests, die für die Methode im Test-Generator angelegt wurden. • types: Eine Liste an Datentypen, welche mit dem TestItem assoziiert werden. Dies beinhaltet die Klasse, in welcher sich die Methode befindet, die Typen der Parameter des Rückgabewerts, der geworfenen Exceptions und der Standard Exceptions der jeweiligen Sprache. Zusätzlich natürlich alle Typen, die im Konstruktor eines anderen Typs benötigt werden. 2. Ports dienen zur Definition von Parametern der Eingabe- und Ausgabewerte. • id: Eine fortlaufende Zahl um den Port eindeutig identifizieren zu können. • name: Der Name des Parameters der Methode, beziehungsweise “expected” für den Rückgabewert und “instance” für die Instanz bei nicht statischen Me- thoden. • fullTypeName: Der voll qualifizierte Name des Typs, um ihn in der Liste der Typen zu finden. • actualParameterModifier: Ein Modifier, der dem Parameter zusätzliche Bedeutung gibt. Wwird in Java nicht benötigt und daher nicht gesetzt. 20
3. Test ist im Prinzip eine Test-Suite und damit eine Ansammlung von Tests. • name: Der Name der Test-Suite. • description: Eine Beschreibung der Test-Suite. • date: Das Datum, wann der Test erstellt wurde. • testCases: Eine Liste aus TestCases. 4. testCase ist ein konkreter Testfall. • name: Der Name des Testfalls. • description: Eine Beschreibung des Testfalls. Muss nicht gesetzt werden. • inputs: Eine Liste von Testdaten, welche die Inputdaten für den Testfall beschreibt. • output: Das erwartete Ergebnis des Testfalls. • exception Die erwartete Exception des Testfalls (an Stelle des “output”). 5. TestData ist der Wert eines Ports für einen Testfall. 6. Exception ist eine geworfene Exception für einen Testfall. Der Typname muss in der Liste der Typen enthalten sein. • value: Der Wert für den Testfall. Die Struktur der Instanz muss der Typde- finition des Typen an diesem Port entsprechen. • portId: Die Referenz auf einen Port. 21
4 Implementierung Devmate besteht im Groben aus 3 Teilen, welche relativ unabhängig voneinander entwi- ckelt werden können: 1. Model-Builder: Besteht aus Parser und Mapper welche IDE- und sprachabhän- gig sind. Diese sorgen dafür, dass ein TestItem-Objekt erstellt wird, das alle In- formationen aus der Methodensignatur, der zu testenden Methode, enthält, welche benötigt werden um den Test-Generator zu starten (siehe Abschnitt 4.1). 2. Test-Generator: Dieser Teil ist IDE- und sprachunabhängig und daher kann der bestehende Code übernommen werden. Die meiste Interaktion mit dem Benutzer erfolgt hier, da er Äquivalenzklassen definieren und ihre Repräsentanten erstellen kann. Zusätzlich werden hier auch die Tests, entweder vom Benutzer oder auch automatisch, erstellt (siehe Abschnitt 4.2). 3. Code-Generator: Mithilfe der definierten Tests, wird dann gültiger Test-Code erstellt und in einer Datei gespeichert (siehe Abschnitt 4.3). Zusätzlich wurde in dieser Masterarbeit ein Plug-in für die gewählte IDE implemen- tiert, welches all diese Teile und die Kommunikation zwischen diesen, enthält. In Zuge dieser Arbeit wurden Plug-ins für IntelliJ und Eclipse entwickelt. Es existieren bereits Pläne für weitere IDEs (siehe Abschnitt 5.2). In diesem Kapitel wird die in Listing 4 zu sehende Methode, “CheckAppointment” der Klasse “AppointmentChecker”, als Beispiel genommen. Listing 4: zu testende Methode 1 package meetingCalculator; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class AppointmentChecker { 7 public MeetingAppointment[] currentCalenderEntries; 8 9 public AppointmentChecker(MeetingAppointment[] currentCalendarEntries) { 10 this.currentCalenderEntries = currentCalendarEntries; 11 } 12 13 // die zu testende Methode 14 public boolean CheckAppointment(MeetingAppointment meeting) throws Exception { 15 if (currentCalenderEntries == null) throw new Exception("Calendar can not be null"); 16 if (meeting == null) return false; 23
17 18 for (MeetingAppointment cur : currentCalenderEntries) { 19 if (cur.getStartTime().isAfter(cur.getEndTime())) throw new Exception(" Start must be before End"); 20 if (meeting.getStartTime().isAfter(meeting.getEndTime())) throw new Exception("Start Time of Meeting2Check must be before End Time"); 21 22 if (meeting.getStartTime().isAfter(cur.getStartTime()) && meeting. getStartTime().isBefore(cur.getEndTime())) { 23 return false; 24 } 25 if (cur.getStartTime().isAfter(meeting.getStartTime()) && cur.getStartTime ().isBefore(meeting.getEndTime())) { 26 return false; 27 } 28 } 29 return true; 30 } 31 } 4.1 Model-Builder Der Model-Builder besteht aus Parser und Mapper. Der Parser sorgt dafür, dass aus der Methodensignatur ein AST erstellt wird. Hier konnten die bereits zur Verfügung ste- henden Tools der jeweiligen IDEs benutzt werden. Es wird dann ein TestItem-Objekt erstellt. Dieses bekommt die Informationen des spezifischen ASTs und der Mapper sorgt dann dafür, dass jeder Typ auf den passenden generischen Typ der Klasse Type, abgebil- det wird. Beim TestItem wird tests noch nicht gesetzt, da das ja erst im Test-Generator passiert. Daher müssen nur die Ports für die Parameter und den Rückgabewert gesetzt, die Typen definiert, sowie die einfachen Felder des TestItems gesetzt werden (siehe Ab- bildung 12). Die Felder des TestItems werden dabei wie folgt gesetzt: • id: Eine UUID wird zufällig angelegt. • name: Der Name der zu testenden Methode. • uri: Der Speicherort der Klasse wurde beim Event, welches den Parser startet, mitgegeben. • isStatic: Die Modifiers der Methode werden nach dem Keyword “static” durch- sucht. Der Wert ist true, wenn er gefunden wurde, ansonsten ist er false. • inputs: Für jeden Parameter wird ein Port angelegt und in dieser Liste gespeichert. 24
Abbildung 12: Teil der TestItem Klasse, der hier relevant ist. Zusätzlich wird als erstes die Instanz der Klasse gespeichert, wenn die Methode nicht statisch ist. • outputs: Für den Rückgabewert wird auch ein Port angelegt. • tests: Wird hier noch nicht gesetzt, sondern erst im Test-Generator. • types: Eine Liste der gefundenen Typen. Intern wird zuerst eine Map gebildet, welche den vollständigen Namen als Key verwendet. Sobald alle Typen gefunden wurden, wird hier das Value-Set als Liste übergeben. Die Ports für Parameter, Rückgabewert und eventuell die Instanz werden folgender- maßen angelegt: • id: Es wird ein statischer Zähler verwendet, der bei 0 beginnt und bei jeder Initia- lisierung der Klasse Port um eins erhöht wird. • name: Der Name des Parameters der Methode, beziehungsweise “expected” für den Rückgabewert und “instance” für die Instanz bei nicht statischen Methoden. • fullTypeName: Der TypMapper gibt den GAST Typ für diesen Typ zurück. Hier wird dessen voller Name gespeichert, um den Typ später wieder zu finden. • actualParameterModifier: Wird nicht benötigt. Die weiteren Schritte des Parsers und Mappers sind abhängig von der IDE und werden in den nächsten Abschnitten erklärt. 25
Abbildung 13: Test with Equivalence Class Method 4.1.1 Eclipse Für das Eclipse Plug-in wurde als Parser der ASTParser von Eclipse JDT [7] verwen- det. Damit lässt sich ganz einfach ein AST der Java-Klasse erstellen, in der sich die zu testende Methode befindet. Zusätzlich erlaubt er auch, Bindungen aufzulösen, um die verwendeten Typen ihren Definitionen zuzuweisen, da dies benötigt wird. Gestartet wird Devmate, indem mit Rechtsklick im Kontextmenü, die Option “Test with Equivalence Class Method” ausgewählt wird (siehe Abbildung 13). Dies löst ein Eclipse Event aus, welches den Parser mit den Parametern file (die Datei in der das Event ausgelöst wurde) und cursorPosition (die Position des Cursors in dem Moment) startet. Eclipse JDT baut einen AST über die Datei auf und stellt alle Methoden dieser Klasse bereit. Dann wird die zu testende Methode anhand der Position des Cursors gefunden oder eine Fehlermeldung ausgegeben, falls das Event außerhalb einer Methode ausgeführt wurde. In Listing 5 ist der erstellte AST für die Klasse “AppointmentChecker” zu sehen (siehe Listing 4 für den Code). Die Körper der Methoden wurden hier aber ausgelassen, da sie für das Beispiel irrelevant sind. Listing 5: Der erzeugte AST des Beispiels. 1 TypeDeclaration: AppointmentChecker 2 Modifiers: {public} 3 Modifier: public 4 interface: false 5 name = SimpleName: "AppointmentChecker" 6 Bodydeclarations: 7 FieldDeclaration: currentCalendarEntries 8 Modifiers: {public} 9 Modifier: public 10 type = ArrayType: MeetingAppointment[] 11 elementType = SimpleType: 12 name = SimpleName: "MeetingAppointment" 13 Fragments: 26
14 VariableDeclarationFragment: 15 name = SimpleName: "currentCalendarEntries" 16 MethodDeclaration: AppointmentChecker 17 Modifiers: {public} 18 Modifier: public 19 constructor = true 20 name = SimpleName: AppointmentChecker 21 Parameters 22 SingleVariableDeclaration: 23 type = ArrayType: MeetingAppointment[] 24 elementType = SimpleType: 25 name = SimpleName: "MeetingAppointment" 26 name = SimpleName: "currentCalendarEntries" 27 body = {...} 28 MethodDeclaration: CheckAppointment 29 Modifiers: {public} 30 Modifier: public 31 constructor = false 32 returnType2 = PrimitiveType 33 primitiveTypeCode: boolean 34 name = SimpleName: "CheckAppointment" 35 Parameters 36 SingleVariableDeclaration: 37 type = ArrayType: MeetingAppointment[] 38 elementType = SimpleType: 39 name = SimpleName: "MeetingAppointment" 40 name = SimpleName: "meeting" 41 ThrownExceptionsTypes: 42 SimpleType: 43 name = SimpleName: "Exception" 44 body = {...} Der TypeMapper wird für jeden gefundenen Typ im AST aufgerufen. Er überprüft zuerst ob der mitgegebene Typ schon in der TypMap vorhanden ist. Sollte das nicht der Fall sein, wird die Bindung des Typs aufgelöst, um die Klasse des Typen zu bekommen. Mit dieser Information wird ein neuer Typ erstellt, der in die TypMap eingefügt wird. Die Klasse “org.eclipse.jdt.core.dom.AST” kennt unter anderem alle primitiven Typen, sowie auch die Klasse “java.lang.String”, welche im GAST wie ein primitiver Typ behandelt wird. Daher kann für diese bekannten Typen direkt eine Instanz des passenden Typs im GAST angelegt werden. Alle Informationen, die der entsprechende Typ benötigt, sind direkt im Code verankert, da sie für diese Typen immer gleich sind. Ein Beispiel für so eine Mapper-Methode ist in Listing 6 zu sehen. Listing 6: IntegerMapper 1 Type mapInteger() { 2 IntegerType type = new IntegerType(); //der Typ im GAST 3 //der Name des Typs im GAST 4 type.setAstTypeName("IntegerType"); 27
5 type.setFullName("int"); //der voll qualifizierte Name 6 type.setName("int"); //der einfache Name 7 type.setNullable(false); //ob der Typ ``null'' sein darf 8 type.setSign(true); //in Java immer wahr 9 type.setSize(32); //der Speicher in Bits 10 type.setThrowable(false); //nur bei Exceptions wahr 11 return type; 12 } Sollte der gesuchte Typ keinem der primitiven Typen entsprechen, wird überprüft, ob es sich um ein Array oder ein Enum handelt. Wenn auch dies nicht zutrifft, wird der allgemeine ClassMapper aufgerufen. Im ArrayMapper wird ein ArrayType angelegt. Der Typ des Elements wird wieder im TypeMapper bestimmt und dessen Name als Referenz in ElementFullTypeName ge- speichert (siehe Listing 7). Listing 7: ArrayMapper 1 Type mapArray(ITypeBinding binding) { 2 //mapping des ElementTyps 3 Type elementType = typeMapper.map(binding.getElementType()); 4 ArrayType type = new ArrayType(); 5 type.setAstTypeName("ArrayType"); 6 type.setFullName(binding.getQualifiedName()); 7 type.setName(binding.getName()); 8 type.setElementFullTypeName(elementType.getFullName()); 9 type.setNullable(true); 10 type.setThrowable(false); 11 return type; 12 } Im EnumMapper werden die Enum-Literale, welche im AST als Felder der Klasse vor- handen sind, mit ihrem Namen als Wert in einer Liste gespeichert (siehe Listing 8). Listing 8: EnumMapper 1 Type mapEnum(ITypeBinding binding) { 2 EnumType type = new EnumType(); 3 type.setAstTypeName("EnumType"); 4 type.setName(binding.getName()); 5 type.setFullName(binding.getQualifiedName()); 6 type.setNullable(true); 7 type.setEnumLiterals(getEnumerators(binding)); 8 return type; 9 } 10 11 List getEnumerators(ITypeBinding binding) { 12 ArrayList list = new ArrayList(); 13 if (binding.isEnum()) { 14 IVariableBinding[] fields = binding.getDeclaredFields(); 28
15 for (IVariableBinding field : fields) { 16 list.add(new EnumLiteral().value(field.getName())); 17 } 18 } 19 return list; 20 } Wenn keiner der vorher genannten Typen passt, wird der allgemeine ClassMapper auf- gerufen. Im ClassMapper wird der Name und der voll qualifizierte Name der Klasse, gespeichert. Um später Instanzen der Klasse erzeugen zu können, werden alle Konstruk- toren die nicht “private” sind, gespeichert. Dort müssen dann auch jeweils die Parameter des Konstruktors mitgegeben und neue Typen, die dort vorkommen, gemappt werden. Der ClassMapper ist in Listing 9 zu sehen. Listing 9: ClassMapper 1 Type mapClass(ITypeBinding binding) { 2 ClassType type = new ClassType(); 3 type.setAstTypeName("ClassType"); 4 type.setName(binding.getName()); 5 type.setFullName(binding.getQualifiedName()); 6 type.setNullable(true)); 7 type.setThrowable(false); 8 9 boolean isPublicConstructor; 10 IMethodBinding[] methods = binding.getDeclaredMethods(); 11 for (int i=0; i
Abbildung 14: Test with Devmate Geworfene Exceptions sind zwar auch ClassTypes, haben aber ihren eigenen Mapper. Dort wird “throwable” auf “true” gesetzt. Konstruktoren werden nicht gespeichert, da diese nicht benötigt werden. Dieser Mapper wird anders aufgerufen, da Exceptions nur nach dem “throws” in der Methodensignatur, vorkommen dürfen. In Listing 10 wird der Code im Parser gezeigt, der es erlaubt, die Exceptions zu mappen. Listing 10: Die geworfenen Exceptions der Methode werden gemappt 1 for (Object o : methodDeclaration.thrownExceptionTypes()) { 2 if (o instanceof SimpleType) { 3 addExceptionType((SimpleType) o); 4 } 5 } 4.1.2 IntelliJ IntelliJ benutzt das Program Structure Interface (PSI). PSI ist eine zusätzliche Schicht, welche verantwortlich für das Parsen der Dateien ist und die das syntaktische und seman- tische Codemodell erstellt [11]. Dies erleichtert die Verwendung des darunter liegenden ASTs. Das Tool wird gestartet, wenn im Kontextmenü die Funktion “Test with Devmate” ausgewählt wird (siehe Abbildung 14). Dies löst eine Aktion aus, welche den Java-Parser mit der zu testenden Methode startet oder einen Fehler ausgibt, falls das Kommando außerhalb einer Methode gestartet wurde. In Listing 11 ist die erstellte PSI-Struktur für die Klasse “AppointmentChecker” zu sehen (siehe Listing 4 für den Code). Die Körper der Methoden wurden hier aber ausge- lassen, da sie für das Beispiel irrelevant sind. 30
Listing 11: Die erzeugte PSI-Struktur des Beispiels. 1 PsiClass: AppointmentChecker 2 PsiModifierList: public 3 PsiIdentifier: "AppointmentChecker" 4 PsiJavaToken:LBRACE 5 PsiField: currentCalendarEntries 6 PsiModifierList: public 7 PsiTypeElement: MeetingAppointment[] 8 PsiTypeElement: MeetingAppointment 9 PsiIdentifier: "MeetingAppointment" 10 PsiIdentifier: "currentCalendarEntries" 11 PsiMethod: AppointmentChecker 12 PsiModifierList: public 13 constructor = true 14 PsiIdentifier: AppointmentChecker 15 PsiParametersList:(MeetingAppointment[] currentCalendarEntries) 16 PsiParameter: currentCalendarEntries 17 PsiTypeElement: MeetingAppointment[] 18 PsiTypeElement: MeetingAppointment 19 PsiIdentifier: "MeetingAppointment" 20 PsiIdentifier: "currentCalendarEntries" 21 PsiCodeBlock = {...} 22 PsiMethod: CheckAppointment 23 PsiModifierList: public 24 PsiTypeElement: boolean 25 PsiIdentifier: "CheckAppointment" 26 PsiParametersList:(MeetingAppointment meeting) 27 PsiParameter: currentCalendarEntries 28 PsiTypeElement: MeetingAppointment 29 PsiIdentifier: "MeetingAppointment" 30 PsiIdentifier: "meeting" 31 PsiReferenceList: 32 PsiJavaCodeReferenceElement: Exception 33 PsiIdentifier: "Exception" 34 PsiCodeBlock = {...} 35 PsiJavaToken:RBRACE Der TypeMapper wird für jeden gefundenen Typ im AST aufgerufen. Er überprüft zuerst, ob der mitgegebene Typ schon in der TypMap vorhanden ist. Sollte das nicht der Fall sein, wird ein neuer Typ erstellt, der in die TypMap eingefügt wird. Dazu wird als erstes geprüft, ob der Typ ein Array ist oder einem primitiven Typen entspricht. Dazu wird dieser mit den in “com.intellij.psi.PsiType” gespeicherten Konstanten verglichen. Für die primitiven Typen kann direkt eine Instanz des passenden Typs im GAST ange- legt werden. Alle Information, die der entsprechende Typ benötigt, sind direkt im Code verankert, da sie für diese Typen immer gleich sind. Ein Beispiel für so eine Mapper- Methode ist in Listing 12 zu sehen. Listing 12: IntegerMapper 31
1 Type mapInteger() { 2 IntegerType type = new IntegerType(); //der Typ im GAST 3 //der Name des Typs im GAST 4 type.setAstTypeName("IntegerType"); 5 type.setFullName("int"); //der voll qualifizierte Name 6 type.setName("int"); //der einfache Name 7 type.setNullable(false); //ob der Typ ``null'' sein darf 8 type.setSign(true); //in Java immer wahr 9 type.setSize(32); //der Speicher in Bits 10 type.setThrowable(false); //nur bei Exceptions wahr 11 return type; 12 } Im ArrayMapper wird ein ArrayType angelegt. Der Typ des Elements wird wieder im TypeMapper bestimmt und der Name als Referenz in ElementFullTypeName gespeichert (siehe Listing 13). Listing 13: ArrayMapper 1 Type mapArray(PsiArrayType arrayType) { 2 Type elementType = typeMapper.map(arrayType.getComponentType()); //mapping des ElementTyps 3 ArrayType type = new ArrayType(); 4 type.setAstTypeName("ArrayType"); 5 type.setFullName(arrayType.getCanonicalText()); 6 type.setName(arrayType.getPresentableText()); 7 type.setElementFullTypeName(elementType.getFullName()); 8 type.setNullable(true); 9 type.setThrowable(false); 10 return type; 11 } Sollte der Typ weder ein Array sein noch einem der primitiven Typen entsprechen, wird dieser aufgelöst, um die dahinter-liegende Klasse zu bekommen. Diese kann nun entweder ein Enum sein, der Klasse “java.lang.String” (welche im GAST als primitiver Typ verwendet wird) oder einer anderen Klasse entsprechen . Im EnumMapper werden die Enum-Literale, welche in der PSI-Struktur als Felder der Klasse vorhanden sind, mit ihrem Namen als Wert in einer Liste gespeichert (siehe Listing 14). Listing 14: EnumMapper 1 Type mapEnum(PsiClass psiClass) { 2 EnumType type = new EnumType(); 3 type.setAstTypeName("EnumType"); 4 type.setName(psiClass.getName()); 5 type.setFullName(psiClass.getQualifiedName()); 6 type.setNullable(true); 7 type.setEnumLiterals(getEnumerators(psiClass)); 32
8 return type; 9 } 10 11 List getEnumerators(PsiClass psiClass) { 12 ArrayList list = new ArrayList(); 13 PsiField[] fields = psiClass.getFields(); 14 for (PsiField field : fields) { 15 list.add(new EnumLiteral().value(field.getName())); 16 } 17 return list; 18 } Wenn keiner der vorher genannten Typen passt, wird der allgemeine ClassMapper aufgerufen. Im ClassMapper wird der Name und der voll qualifizierte Name der Klasse gespeichert. Um später Instanzen der Klasse erzeugen zu können, werden alle Konstruk- toren die nicht “private” sind, gespeichert. Dort müssen dann auch jeweils die Parameter des Konstruktors mitgegeben und neue Typen, die dort vorkommen, gemappt werden. Der ClassMapper ist in Listing 15 zu sehen. Listing 15: ClassMapper 1 Type mapClass(PsiClass psiClass) { 2 ClassType type = new ClassType(); 3 type.setAstTypeName("ClassType"); 4 type.setName(psiClass.getName()); 5 type.setFullName(psiClass.getQualifiedName()); 6 type.setNullable(true); 7 type.setThrowable(false); 8 9 boolean isPublicConstructor; 10 for (PsiMethod method : psiClass.getConstructors()) { 11 isPublicConstructor = !method.getModifierList().hasModifierProperty(PsiModifier. PRIVATE); 12 if (isPublicConstructor) { 13 type.addConstructorsItem(new Constructor().parameters(createMemberList(( PsiParameter[]) method.getParameters()))); 14 } 15 } 16 return type; 17 } 18 19 private List createMemberList(PsiParameter[] parameters) { 20 List memberList = new ArrayList(); 21 for (PsiParameter parameter : parameters) { 22 PsiType type = parameter.getTypeElement().getType(); 23 Member member = new Member(); 24 member.setName(parameter.getName()); 25 member.setFullTypeName(type.getCanonicalText()); 26 memberList.add(member); 27 // Falls der Typ neu ist, wird er hier erstellt und gespeichert. 33
28 typeMapper.map(type); 29 } 30 return memberList; 31 } Geworfene Exceptions sind zwar auch ClassTypes, haben aber ihren eigenen Mapper. Dort wird “throwable” auf “true” gesetzt. Konstruktoren werden nicht gespeichert, da diese nicht benötigt werden. Dieser Mapper wird anders aufgerufen, da Exceptions nur nach dem “throws” in der Methodensignatur vorkommen dürfen. In Listing 16 wird der Code im Parser gezeigt, der es erlaubt, die Exceptions zu mappen. Listing 16: Die geworfenen Exceptions der Methode werden gemappt 1 PsiClassType[] exceptions = (PsiClassType[]) method.getThrowsTypes(); 2 for (PsiType type : exceptions) { 3 addException(type); 4 } 4.1.3 Beispiel In den vorherigen Abschnitten wurde die von der IDE erstellte Struktur für ein Beispiel gezeigt (in Listing 5 und Listing 11). Danach wurde angegeben, wie ein TestItem in der jeweiligen IDE angelegt werden kann. In Listing 17 ist nun das erstellte TestItem zu sehen, welches in beiden Fällen gleich ist (mit Ausnahme des Attributs “id”, da dieses zufällig vergeben wird). Listing 17: Beispiel für ein erstelltes TestItem 1 TestItem: CheckAppointment 2 id: "7fa61db3-277e-4585-b127-44eef0f09c98" 3 name: "CheckAppointment" 4 uri: "C:\Users\Jakob\IdeaProjects\Demo\src\meetingCalculator" 5 isStatic: false 6 tests: List 7 inputs: List 8 Port: AppointmentChecker instance 9 id: 0 10 name: "instance" 11 fullTypeName: meetingCalculator.AppointmentChecker 12 actualParameterModifier: null 13 Port: MeetingAppointment meeting 14 id: 1 15 name: "meeting" 16 fullTypeName: meetingCalculator.MeetingAppointment 17 actualParameterModifier: null 18 output: Port 19 Port: boolean expected 20 id: 2 34
21 name: "expected" 22 fullTypeName: boolean 23 actualParameterModifier: null 24 types: List 25 Type: meetingCalculator.AppointmentChecker 26 fullName: "meetingCalculator.AppointmentChecker" 27 name: "AppointmentChecker" 28 astTypeName: "classType" 29 nullable: true 30 throwable: false 31 DataType: 32 AggregateType: 33 ClassType: 34 constructors: List 35 Constructor: (MeetingAppointment[] currentCalendarEntries) 36 parameters: List 37 Member: MeetingAppointment[] currentCalendarEntries 38 name: "currentCalendarEntries" 39 fullTypeName: "meetingCalculator.MeetingAppointment[]" 40 Type: meetingCalculator.MeetingAppointment[] 41 fullName: "meetingCalculator.MeetingAppointment[]" 42 name: "MeetingAppointment[]" 43 astTypeName: "ArrayType" 44 nullable: true 45 throwable: false 46 DataType: 47 ConstuctedType: 48 elementFullTypeName: "meetingCalculator.MeetingAppointment" 49 ArrayType 50 Type: meetingCalculator.MeetingAppointment 51 fullName: "meetingCalculator.MeetingAppointment" 52 name: "MeetingAppointment" 53 astTypeName: "ClassType" 54 nullable: true 55 throwable: false 56 DataType: 57 AggregateType: 58 ClassType: 59 constructors: List 60 Constructor: (DateTime startTime, DateTime endTime) 61 parameters: List 62 Member: DateTime startTime 63 name: "startTime" 64 fullTypeName:"meetingCalculator.DateTime" 65 Member: DateTime endTime 66 name: "endTime" 67 fullTypeName:"meetingCalculator.DateTime" 68 Constructor: (DateTime startTime, DateTime endTime, String appointmentText) 69 parameters: List 70 Member: DateTime startTime 71 name: "startTime" 35
72 fullTypeName:"meetingCalculator.DateTime" 73 Member: DateTime endTime 74 name: "endTime" 75 fullTypeName:"meetingCalculator.DateTime" 76 Member: String appointmentText 77 name: "appointmentText" 78 fullTypeName: "java.lang.String" 79 Type: meetingCalculator.DateTime 80 fullName: "meetingCalculator.DateTime" 81 name: "DateTime" 82 astTypeName: "ClassType" 83 nullable: true 84 throwable: false 85 DataType: 86 AggregateType: 87 ClassType: 88 constructors: List 89 Constructor: (int year, int month, int day, int hour, int minute) 90 parameters: List 91 Member: int year 92 name: "year" 93 fullTypeName: int 94 Member: int month 95 name: "month" 96 fullTypeName: int 97 Member: int day 98 name: "day" 99 fullTypeName: int 100 Member: int hour 101 name: "hour" 102 fullTypeName: int 103 Member: int minute 104 name: "minute" 105 fullTypeName: int 106 Type: int 107 fullName: "int" 108 name: "int" 109 astTypeName: "IntegerType" 110 nullable: false 111 throwable: false 112 DataType: 113 PrimitiveType: 114 NumberType: 115 sign: true 116 IntegralType: 117 size: 32 118 IntegerType 119 Type: java.lang.String 120 fullName: "java.lang.String" 121 name: "String" 122 astTypeName: "StringType" 123 nullable: true 36
124 throwable: false 125 DataType: 126 PrimitiveType: 127 StringType 128 Type: boolean 129 fullName: "boolean" 130 name: "boolean" 131 astTypeName: "BooleanType" 132 nullable: false 133 throwable: false 134 DataType: 135 PrimitiveType: 136 BooleanType 137 Type: Excepton 138 fullName: "java.lang.Exception" 139 name: "Excepton" 140 astTypeName: "ClassType" 141 nullable: true 142 throwable: true 143 DataType: 144 AggregateType: 145 ClassType: 146 constructors: null 4.2 Test-Generator Der Test-Generator ist die hauptsächliche Benutzeroberfläche von Devmate (siehe Ab- schnitt 3). Da dieser Teil sprachunabhängig ist, konnte er für diese Arbeit komplett übernommen werden. Der Test-Generator läuft auf einem Server, der mit dem Plug-in gestartet wird, wobei die Kommunikation über Web-Sockets erfolgt. Beim ersten Start, wird der Benutzer gefragt, wo er die Konfigurationsdatei (“.tmdl”) speichern will. In dieser wird das zuvor generierte TestItem-Objekt, in Base64-kodiert, gespeichert. Aus dieser Datei wird eine URL (Uniform Resource Locator) generiert. Diese kann nun in einem Browser aufgerufen werden, um das Objekt zu laden und auch bei jeder Änderung auto- matisch das aktuelle TestItem-Objekt in der Konfigurationsdatei zu speichern. Um die Benutzerfreundlichkeit zu erhöhen, wird bei den Plug-ins ein Browser mitgeliefert, der in die IDE eingebettet, ausgeführt wird. Somit muss nicht zwischen IDE und einem exter- nen Browser gewechselt werden, um Devmate zu benutzen. Für die Kommunikation über die Web-Sockets, gibt es eine Schnittstelle die in jeder IDE implementiert werden muss, welche das Speichern und Laden der Konfigurationsdatei, sowie das Erzeugen der URL beinhaltet. Die Einbettung des Browsers wird auch für jede IDE extra vorgenommen. Ansonsten muss dieser Teil von Devmate nicht auf IDE oder Sprache angepasst werden. Es kann daher der bereits vorhandene Code verwendet werden. 37
Abbildung 15: Test-Generator mit 4 erstellten Testfällen Im Test-Generator kann der Benutzer Äquivalenzklassen definieren und ihre Reprä- sentanten erstellen. Danach kann er sich Testfälle generieren lassen oder auch eigene erstellen. Dafür muss für jeden Testfall ein erwarteter Rückgabewert, beziehungsweise die erwartete, geworfene Exception, definiert werden. Es besteht auch die Möglichkeit, eine vorher erstellte Konfigurationsdatei zu laden, solange sich die Methodensignatur nicht geändert hat, damit nicht immer alle Einstellungen erneut vorgenommen werden müssen. In Abbildung 15 ist der Test-Generator in Benutzung zu sehen. Es wurden zwei Äquivalenzklassen für das AppointmentChecker-Objekt (“this”) erstellt und drei für den Parameter “meeting” des Typs MeetingAppointment. Für jede dieser Klassen wurde ein Repräsentant erstellt, der mit einem der bestehenden Konstruktoren erstellt wurde (siehe Abbildung 16). Es besteht auch die Möglichkeit eine eigene Factory-Methode zu erstellen, falls die Konstruktoren nicht ausreichend sein sollten (siehe Abbildung 17). Das TestItem-Objekt wird dabei nur insofern verändert, dass nun auch “tests” gesetzt ist. Der Rest bleibt dabei gleich. In Listing 18 sind nun für das Beispiel, die testCases des TestItems zu sehen, welche im TestGenerator angelegt wurden. Um das Format nicht zu sprengen, wurden dabei aber weniger interessante Teile ausgelassen. Das vorherige TestItem kann in Listing 17 gefunden werden. 38
Sie können auch lesen