Devmate: Generierung von JUnit- Java- JKU ...

Die Seite wird erstellt Aaron Maier
 
WEITER LESEN
Devmate: Generierung von JUnit- Java- JKU ...
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
Devmate: Generierung von JUnit- Java- JKU ...
Devmate: Generierung von JUnit- Java- JKU ...
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
Devmate: Generierung von JUnit- Java- JKU ...
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