Testfall-Generierung zur Erkennung semantischer Änderungen in

Werbung
FernUniversität in Hagen
Fakultät für Mathematik und Informatik
Lehrgebiet Programmiersysteme
Testfall-Generierung
zur Erkennung semantischer Änderungen
in Metadaten von JPA-Programmen
Abschlussarbeit
im Studiengang Bachelor of Science in Informatik
Verfasserin: Kathi Stutz
Matrikelnummer: 7174330
Betreuer: Bastian Ulke
Abgabetermin: 20. 10. 2015
Inhaltsverzeichnis
Abstract
2
1. Einleitung
1.1 Motivation
1.2 Aufbau derArbeit
2. Problemstellung
2.1 Stand der Technik
2.2 Aufgabenstellung
2.3 Verwandte Arbeiten
3. Lösungsansatz
3.1 Grundlegender Testaufbau
3.2 Annotationen zur Definition von Primärschlüsseln
3.2.1 Einfache Primärschlüssel
3.2.2 Zusammengesetzte Primärschlüssel
3.3 Annotationen zur Definiton von Assoziationen
3.3.1 Kardinalitäten
3.3.2 Das Annotationselement mappedBy
4. Implementierung
4.1 Struktur der erzeugten Dateien
4.1.1 Struktur der Testdateien
4.1.2 Struktur der Hilfsklasse TestUtils.java
4.1.3 Beispiel
4.2 Implementierung mit Acceleo
4.2.1 Einführung in Acceleo
4.2.2 Ablauf der Codegenerierung
5. Ergebnisse
5.1 Evaluation
5.2 Diskussion
6. Schlussbetrachtung
6.1 Ausblick
6.2 Zusammenfassung
3
3
3
5
5
7
9
11
11
11
12
13
15
16
18
23
23
23
26
27
30
30
32
38
38
42
45
45
45
Anhang
47
Literaturverzeichnis
48
1
Abstract
Die vorliegende Bachelorarbeit beschäftigt sich mit der automatischen Generierung von
JUnit-Tests für JPA-Metadaten. Der Fokus liegt dabei auf Annotationen und
Annotationselementen zum Definieren von Primärschlüsseln und Herstellen von
Assoziationen zwischen JPA-Entitäten. Es werden schematische Beschreibungen der
nötigen Testabläufe entwickelt und prototypisch als Eclipse-Plugin im Rahmen des Dr.
Deepfix-Projektes implementiert. Bei der anschließenden Evaluation trat jedoch auch
der starke Einfluss des gewählten Persistenzproviders (in diesem Falle Hibernate)
zutage.
2
1 Einleitung
1.1 Motivation
In der Software-Entwicklung gilt generell: Wenn keine Regressionstests für einen
bestimmten Aspekt des zu entwickelnden Programmes existieren, muss man ihn bei
jeder
vorgenommenen
Modifikationen
des
Änderung
zeitaufwendig
Programmverhaltens
als
manuell
auch
reine
testen.
Da
sowohl
Refaktorisierungen
unbeabsichtigte Auswirkungen haben können, die dann unter Umständen an ganz
anderer Stelle der Software zu Fehlern führen, ist es wünschenswert, über eine
möglichst umfassende Suite von automatisch ausführbaren Tests zu verfügen, die jede
Regression zeitnah zutage treten lassen.
Noch praktischer wäre es natürlich, wenn man diese Testsuite nicht aufwendig manuell
erstellen müsste, sondern sie automatisch generiert werden könnte. Das ist für normalen
objekt-orientierten Code ein sehr komplexes Unterfangen, welches bereits auf
verschiedenen Wegen angegangen wurde1. Diese Abschlussarbeit konzentriert sich
lediglich auf einen Teilaspekt von Softwareprojekten, nämlich die objekt-relationalen
Metadaten, welche zur Persistierung von Objekten über die Laufzeit eines Programms
hinaus benötigt werden. Durch ihre eng umrissene Semantik müsste es möglich sein,
automatisch Tests erzeugen zu lassen, welche eine verhaltensändernde Modifikation
dieser Metadaten aufdecken.
Auf Grundlage dieser Überlegungen werden in der vorliegenden Abschlussarbeit
schematische Testabläufe für ausgewählte Elemente der Java Persistence API (JPA)
entwickelt und prototypisch im Rahmen eines Eclipse-Plugins implementiert, mit
dessen Hilfe Regressionstests für diese Elemente automatisch generiert werden können.
1.2 Aufbau der Arbeit
Das nächste Kapitel erläutert die Problemstellung genauer und bietet eine kurze
Einführung in die technischen Voraussetzungen und verwandte Arbeiten zu diesem
1 Vgl. Unterkapitel 2.3
3
Thema. In Kapitel 3 werden dann einige grundlegende Elemente der JPA vorgestellt und
jeweils das Schema für die zugehörigen Tests entworfen. Kapitel 4 beschreibt die
Implementierung dieser Testschemata, wobei im ersten Teil auf die grundsätzliche
Struktur der erzeugten Dateien eingegangen wird (Unterkapitel 4.1) und im zweiten Teil
dann auf das eigentliche Vorgehen zu ihrer Erzeugung (Unterkapitel 4.2). Es folgt das 5.
Kapitel, welches die Evaluation der erzeugten Tests und ihre Ergebnisse beschreibt
sowie eine Diskussion derselben beinhaltet. Den Abschluss dieser Arbeit bildet das 6.
Kapitel mit einem Ausblick auf mögliche Themengebiete für weiterführende Arbeiten
und einer Zusammenfassung.
4
2 Problemstellung
Dieses
Kapitel
widmet
sich
der
genaueren
Erläuterung
der
eigentlichen
Problemstellung. Dazu wird in Unterkapitel 2.1 zuerst einmal das technische Umfeld
dargestellt, in dem sich meine Arbeit bewegt. Es folgt die konkrete Aufgabenstellung
(Unterkapitel 2.2) sowie ein Überblick über die schon vorhandenen Ansätze und
Entwicklungen auf diesem Gebiet (Unterkapitel 2.3).
2.1 Stand der Technik
Ausgangspunkt und Grundlage dieser Arbeit ist die Java Persistence API (JPA) [4],
welche im Rahmen des Java Community Process als Java Specification Request 317
entwickelt wurde und seit dem 22. April 2013 in Version 2.1 vorliegt. Die JPA wurde
entworfen, um Java-Objekte über die Laufzeit des zugehörigen Programms hinaus
persistieren zu können. Dazu werden sie in einer relationalen Datenbank abgelegt. Eine
Persistenzeinheit (im weiteren Verlauf als “Entität”2 bezeichnet) entspricht dabei einem
einzelnen Java-Objekt, zu dem im Allgemeinen genau eine Datenbanktabelle gehört,
deren Zeilen den Instanzen der betreffenden Klasse entsprechen.
Die Zuordnung von Laufzeitobjekten zu Datenbankeinträgen geschieht durch
objektrelationale Metadaten, die entweder als Annotationen im Java-Quelltext gesetzt
werden oder als Einträge in einer separaten xml-Datei vorliegen [4, S. 511]. In dieser
Bachelorarbeit beschränke ich meine Betrachtung der Anschaulichkeit halber auf
Annotationen, da sie kürzer und leichter lesbar sind. Der von mir entwickelte Ansatz zur
Testgenerierung ist jedoch unabhängig von der konkreten Art der JPA-Metadaten.
Die JPA-Spezifikation definiert zwei sogenannte Zugriffsarten (“Access types”) [4, S.
27], welche sich vor allem in der Platzierung der Annotationen unterscheiden. Beim
Field access werden die einzelnen Instanzvariablen der betreffenden Klasse annotiert,
während beim Property access statt dessen deren Getter-Methoden mit Annotationen
versehen werden. Ich verwende in den Beispielen im Rahmen dieser Arbeit aus Platzund Lesbarkeitsgründen ausschließlich Field access; das von mir entwickelte Plugin
2
In der JPA-Spezifikation: Entity, vgl. [4, S. 23]
5
kann aber ebenso mit JPA-Projekten umgehen, welche Property access als Zugriffsart
verwenden (vgl. Abschnitt 4.1.1).
Das von mir entwickelte Plugin stellt eine Erweiterung des Projekts “Dr. Deepfix” dar,
welches am Beispiel der Java Persistence API einen neuen Ansatz zum Auflösen von
Wohlgeformtheitsfehlern in Software-Projekten untersucht [8]. Während gängige IDEs
bisher lediglich anbieten, einzelne Fehler ohne Beachtung weiterer Zusammenhänge
(und dadurch entstehender neuer Fehler) zu beheben, führt Dr. Deepfix eine andere
Strategie ein. Dabei wird das Projekt als Ganzes betrachtet, seine semantischen Fehler
in Form von verletzten Constraints zusammen erfasst und schließlich mit einem
Constraint solver aufgelöst. Im Zuge dessen wird nach Deep fixes gesucht, welche nicht
nur einzelne verletzte Constraints beheben, sondern das gesamte betrachtete Projekt in
einen konsistenten Zustand überführen.
Im Rahmen des Dr. Deepfix-Projekts wird dieses Vorgehen anhand der JPA als Beispiel
untersucht. Es existiert bereits der Prototyp eines Tools, welches sich als Plugin in die
Eclipse IDE integriert und auf Basis von Constraints Deep fixes für semantische Fehler
in JPA-Projekten anbietet. Darauf baut das von mir entwickelte Plugin auf. Es nutzt
Teile des Dr. Deepfix-Plugins und dient letztlich auch zur Unterstützung der weitere
Entwicklung desselben (s. Unterkapitel 2.2).
Die JPA an sich stellt nur eine Schnittstellendefinition dar, welche erst praktisch
anwendbar
ist,
wenn
Implementierungen
dazu
existieren,
sogenannte
ORM-Frameworks (object-relational mapping framework). Inzwischen gibt es mehrere
solcher Persistenzprovider, z.B. OpenJPA, Hibernate oder EclipseLink. 3 Mein Plugin
wurde auf Basis von Hibernate [13] erstellt, welches bereits im Dr. Deepfix-Projekt
verwendet wird.
Hibernate ist ein von Red Hat entwickeltes, quelloffenes Persistenzframework. Wenn
man davon ausgeht, dass sich Dr. Deepfix ebenso wie mein Plugin auf die JPA selbst
bezieht und alle Persistenzprovider die Spezifikation vollständig implementieren,
3
EclipseLink stellt dabei die Referenzimplementierung dar.
6
müsste das konkret verwendete Framework theoretisch eine untergeordnete Rolle
spielen und beliebig durch ein anderes ersetzbar sein. Wir werden allerdings später noch
sehen, dass das in der Praxis durchaus nicht der Fall ist (s. Kapitel 5).
Die zu erstellenden Tests werden JUnit nutzen, ein Framework für Unit-Tests für die
Programmiersprache Java. [2]
2.2 Aufgabenstellung
Wie
in
der
Einleitung
bereits
erwähnt,
sind
Regressionstests
bei
der
Software-Entwicklung unerlässlich, und es wäre wünschenswert, auch für die
Persistierung von Objekten mittels JPA über Tests zu verfügen. Wenn man nun aber
tatsächlich versuchen sollte, für sämtliche persistente Objekte eines JPA-Projektes Tests
zu schreiben, würde man schnell feststellen, dass sich das Schema der erstellten
Testfälle je Annotation ständig wiederholt. Das legt den Gedanken nahe, auf Basis der
klar umrissenen Semantik einzelner Annotationen oder Annotationselemente Tests
automatisch generieren zu lassen.
Die Annotation @Id zum Beispiel wird genutzt, um einer Entität einen Primärschlüssel
zuzuweisen, ohne den sie in der Datenbank nicht wiedergefunden werden kann. Ein Test
für diese Annotation müsste also immer das Persistieren des betreffenden Objektes in
der Datenbank, gefolgt vom Wiederherstellen aus derselben beinhalten. Wenn @Id vom
ursprünglichen Attribut entfernt oder z. B. durch @GeneratedValue ergänzt wurde,
was die Art des Primärschlüssels ändert (s. Abschnitt 3.2.1), dann würde auch der
betreffende Test fehlschlagen, da die Test-Entität aufgrund des falschen Schlüssels nicht
mehr geladen werden kann. (Tatsächlich werden wir später sehen, dass Hibernate als
Persistenzprovider schon das Ablegen einer Entität ohne Primärschlüssel in der
Datenbank nicht erlaubt, vgl. Unterkapitel 5.1.)
Die Generierung dieser Tests würde dann stattfinden, wenn die JPA-Metadaten sich in
einem
konsistenten
Zustand
befinden
und
das
Projekt
das
angestrebte
Persistenzverhalten aufweist. Dies muss natürlich zuerst durch manuelle Tests verifiziert
werden; danach können die JUnit-Testfälle für die JPA-Annotationen jedoch
7
automatisch erzeugt werden, um sie später nach bloßen Refaktorisierungen des
Programmcodes zur Überprüfung durchlaufen zu lassen. Sobald man aber semantische
Änderungen an den JPA-Metadaten vornimmt, muss man deren Korrektheit zuerst
wieder manuell sicherstellen, ehe man die Regressionstests neu generieren lässt.
Die generierten Tests sollen dabei jedoch nur Veränderungen der JPA-Annotationen
selbst, aber nicht des annotierten Java-Codes bewerten. Damit eignen sie sich nur
bedingt für den Einsatz in gängigen Software-Entwicklungsprojekten, da eventuell
schon eine einfache Refaktorisierung des Java-Codes, z. B. die Umbenennung einer
Variable, zu einem Scheitern der Tests führen könnte. Auf einem speziellen Gebiet
können die erzeugten Tests jedoch eine große Hilfe sein: Nämlich bei der Entwicklung
des oben beschriebenen Tools zum Lösen von Wohlgeformtheitsfehlern in
JPA-Projekten - Dr. Deepfix.
Im Zuge der Fehlerkorrektur werden von Dr. Deepfix nämlich die vorhandenen
JPA-Metadaten
modifiziert.
Um
zur
Entwicklungszeit
die
Qualität
dieser
Modifikationen bewerten zu können, ist eine Testsuite praktisch, die das tatsächliche
Persistenzverhalten eines JPA-Projektes gegen eine bestimmte Erwartung überprüfen
kann. Unter Verwendung der vorher aus den korrekten Annotationen erzeugten Tests
kann man so zum Beispiel evaluieren, inwieweit ein vom Constraint solver
vorgeschlagener Deep fix den ursprünglichen Zustand eines beschädigten JPA-Projektes
wiederherstellt. Vor diesem Hintergrund wurde die Funktionsweise der generierten Tests
auch überprüft (s. Unterkapiel 5.1).
Es ist im Rahmen dieser Arbeit natürlich nicht möglich, Tests für sämtliche
JPA-Annotationen zu entwerfen. Ich beschränke mich daher auf zwei Gruppen von
Ausdrücken, welche grundlegend sind und daher besonders häufig verwendet werden.
Die erste Gruppe besteht aus den Annotationen @Id, @GeneratedValue,
@IdClass und @EmbeddedId, welche zum Definieren des Primärschlüssels einer
Entität benötigt werden. Die zweite Gruppe umfasst die Annotationen @OneToOne,
@OneToMany, @ManyToOne und @ManyToMany sowie das Annotationselement
mappedBy, mit deren Hilfe Assoziationen zwischen Entitäten festgelegt werden.
8
In einem ersten Schritt habe ich für diese Annotationen aufgrund ihrer Semantik
Schemata entwickelt, nach denen die zugehörigen Tests jeweils ablaufen sollen. Diese
sind im nächsten Kapitel beschrieben. Darauf aufbauend habe ich ein Plugin für Eclipse
implementiert, welches für bestehende JPA-Projekte eben diese Tests automatisch
erstellt (s. Kapitel 4).
2.3 Verwandte Arbeiten
Da einerseits umfassende Tests für die Entwicklung qualitativ hochwertiger Software
unerlässlich sind, andererseits aber das manuelle Erstellen aussagekräftiger und
umfassender Tests mit viel Aufwand verbunden ist, gibt es zahlreiche Bestrebungen, die
Generierung von Softwaretests zu automatisieren. Gerade für das Gebiet der Unit-Tests,
welche sich jeweils auf eine kleine Einheit innerhalb des Gesamtprogramms beziehen,
finden sich die verschiedensten Ansätze, oftmals begleitet von prototypischen
Implementierungen entsprechender Tools zur Testerzeugung.
Dabei werden die verschiedensten Verfahren genutzt, um zu sinnvollen Testdaten und
-orakeln zu gelangen. Randoop [10] zum Beispiel kombiniert eine Zufallsstrategie zur
Erzeugung von Konstruktor- und Methodenaufrufen mit der Überwachung ihrer
Ausführung, um daraus sinnvolle Assertions abzuleiten. Palus [15] führt erst eine
dynamische Analyse eines Programmdurchlaufs aus, um dann durch eine statische
Analyse relevante Zusammenhänge zwischen Methodenaufrufen und Feldbelegungen
zu erhalten. EvoSuite [7] erstellt auf Basis einer statischen Analyse zuerst Testsuites, die
eine möglichst hohe Testabdeckung erreichen, und generiert dann dazu die passenden
Testorakel. TestFul [1] folgt einem suchbasierten Ansatz...
Diese Liste ließe sich noch weiter fortsetzen, allerdings ist allen eben genannten Tools
und Prototypen gemein, dass sie sich lediglich auf Java-Code beziehen, aber eben nicht
für JPA-Metadaten anwendbar sind. Tatsächlich scheint es bisher keinerlei Ansätze zu
geben, automatische Tests auch für diese zu generieren. Dabei wurde die
Notwendigkeit, die konsistente Verwendung von Metadaten zu überprüfen, durchaus
schon erkannt. [14] zum Beispiel schlägt die Einführung von Invarianten für Metadaten
9
vor, zusammen mit einem Tool, welches die Verletzung derselben erkennt und meldet.
[3] verfolgt einen ähnlichen Ansatz, bei dem die korrekte Verwendung von
Java-Annotationen verifiziert wird. In beiden Fällen wird aber eben nicht mit
Regressionstests gearbeitet.
Auf der anderen Seite existieren auch Testframeworks für die Java Persistence API; das
bekannteste davon ist wohl Arquillian [11]. Diese stellen allerdings lediglich die
Infrastruktur zum Ausführen von JPA-Tests zur Verfügung - die Tests an sich muss
immer noch der Entwickler selbst erstellen.
10
3 Lösungsansatz
3.1 Grundlegender Testaufbau
Eine wichtige Frage, ehe man Tests für die Korrektheit von JPA-Metadaten erstellen
kann, ist natürlich, wie diese Tests grundsätzlich aufgebaut sein sollen. Ihr Zweck
besteht ja darin, Veränderungen der gegebenen Annotationen zu erkennen, die die
Erwartungen bereits bestehenden Codes verletzen. Es soll dabei überprüft werden, ob
die Entitäten des betreffenden Projekts nach der Modifikation noch genauso
„funktionieren“ wie vor dem Eingriff.
Da die grundlegenden Funktionen einer Entität das Speichern in und Laden aus einer
Datenbank sind, soll genau das in den zu erzeugenden Tests durchgegangen werden.
Wenn sich eine Entität dabei problemlos persistieren und wiederherstellen lässt, dann
sind die getesteten Annotationen offensichtlich noch korrekt. Dabei lasse ich bewusst
die
konkret
möglichen
Änderungen
und
die
erwarteten
Reaktionen
des
Persistenzproviders auf fehlerhafte Metadaten oder inkosistente JPA-Projekte außer
Acht. Statt dessen werden Blackbox-Tests erzeugt, deren grundlegendes Schema immer
aus dem Erzeugen einer Entität, ihrem Speichern in der Datenbank und schließlich dem
erneuten Laden aus dieser besteht.
Wie im letzten Kapitel erwähnt, beschränke ich mich dabei auf einige grundlegende
JPA-Metadaten, welche sich in zwei Gruppen einteilen lassen. Das folgende
Unterkapitel (3.2) beschreibt den schematischen Testablauf für Annotationen zur
Definition von Primärschlüsseln (@Id, @GeneratedValue, @IdClass und
@EmbeddedId),
während
sich
Unterkapitel
3.3
einigen Annotationen
und
Annotationselementen zur Definition von Assoziationen widmet (@OneToOne,
@OneToMany, @ManyToOne, @ManyToMany und mappedBy).
3.2 Annotationen zur Definition von Primärschlüsseln
Relationale Datenbanken verwenden Schlüssel, um Datensätze eindeutig identifizieren
zu können. Der Schlüssel einer Tabelle ist eine einzelne Spalte oder eine Gruppe von
11
Spalten, deren Wert bzw. Wertekombination in der ganzen Tabelle nur einmal
vorkommt. Wenn man nun Objekte in einer relationalen Datenbank ablegen will,
benötigt man dazu also zuerst einmal einen Schlüssel. In der JPA-Spezifikation wird
dieser als „Primary key“ bezeichnet und ist eine grundlegende Anforderung an eine
Entität [4, S. 29]. Die JPA-Spezifikation unterscheidet dabei zwischen einfachen und
zusammengesetzten Primärschlüsseln.
3.2.1 Einfache Primärschlüssel
Ein einfacher Primärschlüssel besteht aus dem Wert genau eines Objektattributs,
welches entweder am entsprechenden Feld oder an der zugehörigen Getter-Methode mit
@Id annotiert wird [4, S. 449]. Der Primärschlüssel einer Entität muss gesetzt sein,
damit sie persistiert und später anhand dieses Schlüsselwertes wieder aus der Datenbank
geladen werden kann. Ein sinnvoller Test für die Korrektheit der Annotation @Id sieht
also so aus, dass man in einem ersten Schritt das entsprechende Attribut setzt und die
Entität speichert. Im zweiten Schritt versucht man dann, die Entität über den vorher
vergebenen Schlüsselwert wieder aus der Datenbank zu laden, und überprüft, ob das
erfolgreich war (d. h. das erhaltene Objekt nicht null ist). Die Abbildung 3.1 stellt das
Vorgehen hierzu schematisch dar.
Abbildung 3.1: Schematischer Testablauf für einfache ID
Einfache Primärschlüssel können von Hand gesetzt werden, man kann ihre Generierung
aber auch dem Persistenzframework überlassen. Dazu muss das betreffende Attribut 12
zusätzlich zu @Id - die Annotation @GeneratedValue erhalten, welche außerdem
die Angabe
von
Generierungsstrategien
und
Generatoren
erlaubt,
die
das
Persistenzframework bei der Erzeugung des Schlüsselwertes nutzen soll [4, S. 447]. Da
ich den vom Persistenzframework erzeugten Primärschlüssel jedoch erst nach seiner
Generierung auslese und dann verwende, um das Objekt wieder aus der Datenbank zu
laden, ist die genaue Art seiner Generierung für mein Vorgehen nicht weiter von Belang.
Der schematische Ablauf eines Tests für automatisch generierte Primärschlüssel ist in
Abbildung 3.2 dargestellt.
Abbildung 3.2: Schematischer Testablauf für automatisch generierte ID
3.2.2 Zusammengesetzte Primärschlüssel
Zusammengesetzte Primärschlüssel bestehen im Allgemeinen aus mehreren Attributen
der betreffenden Entität4 und werden immer durch eine eigene ID-Klasse definiert. Die
entsprechenden Werte müssen vom Entwickler gesetzt werden, da die JPA-Spezifikation
die Möglichkeit der Erzeugung per @GeneratedValue nur für einfache
Primärschlüssel fordert.5 Zur Realisierung zusammengesetzter Primärschlüssel, welche
sich meist aus dem Mapping bereits bestehender Datenbanken ergeben, gibt es zwei
Möglichkeiten: Die Verwendung von @IdClass oder @EmbeddedId.
4
5
Trotz des Namens erlaubt die JPA-Spezifikation ausdrücklich auch zusammengesetzte
Primärschlüssel, die nur aus einem Attribut bestehen [4, S. 29]. Für die zu erzeugenden Tests macht
das allerdings keinen Unterschied.
Tatsächlich verbietet sie die automatische Generierung zusammengesetzter Schlüssel nicht.
Allerdings stellt sich die Frage, wozu man mehrere Schlüssel-Attribute braucht, wenn eines davon
schon ein eindeutiger synthetischer Schlüssel ist.
13
In ersterem Fall enthält die betreffende Entität mehrere mit @Id markierte Attribute und
ist selbst mit @IdClass annotiert, gefolgt vom Namen der zugehörigen ID-Klasse,
deren Attribute mit den ID-Attributen der betreffenden Entität in Name und Type
übereinstimmen müssen [4, S. 449]. Zum Persistieren einer solchen Entität genügt es,
sämtliche ihrer ID-Attribute zu setzen. Wenn man sie allerdings aus der Datenbank
laden möchte, benötigt man eine Instanz der zugehörigen ID-Klasse mit entsprechend
gesetzten Attributen. Das Vorgehensschema für Tests der Annotation @IdClass zeigt
die Abbildung 3.3.
Abbildung 3.3: Schematischer Testablauf für IdClass
Als zweite Möglichkeit, einen zusammengesetzten Primärschlüssel zu verwenden, bietet
die JPA-Spezifikation die Annotation @EmbeddedId [4, S. 444]. Dabei werden die
Attribute einer eingebetteten Klasse (annotiert mit @Embeddable) als Primärschlüssel
benutzt. Die Referenz auf diese eingebettete Klasse wird in der umgebenden Entität mit
@EmbeddedId gekennzeichnet.
Während man bei @IdClass die Instanz der ID-Klasse erst zum Laden einer Entität
benötigt, braucht man sie bei @EmbeddedId bereits beim Persistieren, da man den
Primärschlüssel der Entität nur über eine Instanz der eingebetteten Klasse setzen kann.
14
Ein weiterer wichtiger Unterschied zwischen den beiden Arten von zusammengesetzten
Primärschlüsseln besteht darin, dass bei Verwendung von @IdClass die Entität selbst
auch alle ID-Attribute enthält, während sich diese bei @EmbeddedId nur in der
eingebetteten Klasse befinden. Abbildung 3.4 zeigt das Testablauf-Schema für die
Annotation @EmbeddedId.
Abbildung 3.4: Schematischer Testablauf für EmbeddedId
Mit diesen vier Schemata sind alle grundlegenden Arten von Primärschlüsseln, die in
der JPA-Spezifikation vorgesehen sind, abgedeckt. Der Aufbau dieser ID-Tests bildet
die Grundlage für die Assoziationstests, wie wir noch sehen werden.
3.3 Annotationen zur Definition von Assoziationen
Entitäten enthalten meist nicht nur eine ID und persistente einfache Attribute, sondern
auch Referenzen auf andere Entitäten, welche durch die Assoziations-Annotationen
@OneToOne
(1:1-Beziehung),
@OneToMany
(1:n-Beziehung),
@ManyToOne
(n:1-Beziehung) und @ManyToMany (n:m-Beziehung) gekennzeichnet werden [4,
S. 43].
Das grundlegende Vorgehen zum Testen dieser Beziehungen ist wie folgt:
1. Zu testende Entität (“Test-Entität”) instanziieren
2. Referenzierte Entität(en) instanziieren
15
3. Assoziation herstellen durch Setzen des/der entsprechenden Attributs/Attribute
4. Referenzierte Entität(en) persistieren
5. Test-Entität persistieren
6. Test-Entität laden
7. Referenzierte Entität(en) aus Assoziationsattribut auslesen
8. Referenzierte Entität(en) auf null prüfen
Um die dafür benötigten zu testenden und referenzierten Entitäten persistieren zu
können, müssen ihre Primärschlüssel gesetzt sein. Dabei wird für jede Art von
Primärschlüssel auf dieselben Methoden zurückgegriffen, welche bereits in den
ID-Tests benutzt wurden. Auch das Laden der Test-Entität läuft genauso wie bei den
ID-Tests ab. Die referenzierten Entitäten müssen nicht über ihre IDs aus der Datenbank
geladen werden, vielmehr wird dazu das entsprechende Attribut der Test-Entität
ausgelesen und dessen Inhalt auf null überprüft. Schließlich soll mit diesem Test ja die
korrekte Funktion der Assoziationsannotation sichergestellt werden.
In gewisser Weise werden also bei den Assoziationstests die ID-Annotationen der
beteiligten Entitäten mit überprüft, denn wenn diese nicht korrekt sind, wird das
Speichern und Laden der Objekte scheitern. Das zusätzliche Erstellen eines ID-Tests für
jede Entität ist dennoch keine unnütze Wiederholung, sondern durchaus sinnvoll. Man
kann dadurch im Fehlerfall leichter erkennen, welche Annotation die Tests scheitern
lässt: ob nur die Assoziation fehlerhaft ist oder das Problem schon bei der ID beginnt.
3.3.1 Kardinalitäten
Für einen sinnvollen Testaufbau müssen die vier Assoziationen in zwei Gruppen danach
unterteilt werden, auf wie viele referenzierte Entitäten sie verweisen. Bei @OneToOne
und @ManyToOne enthält das zu testende Attribut nämlich die Referenz auf genau eine
andere Instanz, während @OneToMany und @ManyToMany auf mehrere Instanzen
einer Entität (in Form einer Collection oder Map) verweisen.
Dementsprechend muss bei der ersten Gruppe zusätzlich zur Instanz der Test-Entität nur
16
eine Instanz der referenzierten Entität erzeugt und ihre ID gesetzt werden. Ob auf Seiten
der Test-Entität eine oder mehrere Instanzen in der Beziehung vorkommen können (also
der Unterschied zwischen @OneToOne und @ManyToOne) ist für den Testaufbau
unerheblich, da immer genau eine Instanz der zu testenden Entität den Ausgangspunkt
für den Test darstellt. Der vorläufige schematische Ablauf der @...ToOne-Tests ist in
Abbildung 3.5 zu sehen.6
Abbildung 3.5: Schematischer Testablauf für @...ToOne-Tests
Da die zweite Gruppe von Annotationen, nämlich @OneToMany und @ManyToMany
auf eine Collection oder Map7 von Objekten verweisen, muss im Test ein passendes
Container-Objekt mit mehreren Instanzen erzeugt werden. Auch hier ändert der
Unterschied zwischen @OneToMany und @ManyToMany nichts am grundlegenden
Testablauf, der vorläufig so aussieht, wie in Abbildung 3.6 zu sehen, und später
ebenfalls noch für mappedBy angepasst werden muss.
6
7
Er wird später noch für die Verwendung des Annotationselementes mappedBy modifiert werden.
Die JPA-Spezifikation erlaubt die Typen java.util.Collection, java.util.List,
java.util.Set und java.util.Map. [4, S. 25]
17
Abbildung 3.6: Schematischer Testablauf für @...ToMany-Tests
3.3.2 Das Annotationselement mappedBy
Assoziationen sind gerichtet: Wenn eine Entität eine derartige Annotation enthält, die
referenzierte Entität allerdings nicht, handelt es sich um eine unidirektionale
Assoziation. Wenn sich beide Entitäten gegenseitig referenzieren, spricht man von einer
bidirektionalen Assoziation.
Auf Datenbank-Ebene werden Assoziationen im Allgemeinen über Fremdschlüssel
realisiert. Im Falle einer unidirektionalen 1:1-Beziehung zum Beispiel ist klar, dass die
Tabelle
für
die
Entität,
an
welcher
die
Assoziation
markiert
ist,
eine
Fremdschlüsselspalte für die referenzierte Entität enthält. Was aber soll im Falle einer
bidirektionalen 1:1-Assoziation geschehen? Besitzt dann jede Entität in ihrer Tabelle
eine Fremdschlüsselspalte für die jeweils andere Entität?
Um
dieser
Art
von
zirkulärer
Abhängigkeit
vorzubeugen,
verlangt
die
JPA-Spezifikation, dass bei bidirektionalen Assoziationen eine Entität als deren
Eigentümer („Owner“) gekennzeichnet wird. Das geschieht durch Verwendung des
18
Annotationselements mappedBy am korrespondierenden Attribut der anderen (also
Nicht-Eigentümer-)Entität. Als
Wert
von
mappedBy
wird
der
Name
des
Eigentümer-Attributs angegeben [4, S. 43].
Das Vorhandensein des Annotationselements mappedBy erfordert auch eine Änderung
im Testaufbau. Wenn eine Assoziationsannotation dieses Element erhält, bedeutet das
schließlich, dass nicht die Test-Entität selbst, sondern die referenzierte Entität
Eigentümerin der Assoziation ist. Deshalb genügt es nicht, einfach das entsprechende
Attribut der Test-Entität zu setzen, da die Eigentümer-Entität davon nichts erfährt und
die Beziehung so gar nicht erst in die Datenbank gelangt. Um die Assoziation dennoch
zu persistieren, muss das korrespondierende Attribut der referenzierten Entität (dessen
Name ja praktischerweise im mappedBy-Element angegeben wird) gesetzt werden. Da
die Beziehung damit hergestellt ist, kann das Setzen des Attributs der Test-Entität
entfallen.
Das soll im folgenden an einem konkreten Beispiel verdeutlicht werden. Gegeben seien
die beiden Entitäten Employee und Account:
@Entity
public class Employee{
...
@OneToOne
private Account account;
...
}
@Entity
public class Account {
...
@OneToOne(mappedBy=”account”)
private Employee employee;
...
}
In
diesem
Fall
ist
die
Entität
Employee
die
Eigentümerin
der
@OneToOne-Assoziation, da die Entität Account die mappedBy-Markierung
erhalten hat. Das bedeutet also, dass die Datenbanktabelle für Employee einen
19
Fremdschlüssel für den zugehörigen Account enthält. Nun kann man versuchen,
jeweils eine Instanz der beiden Entitäten zu erzeugen und die Assoziation zwischen
ihnen herzustellen:
Account account = new Account();
Employee employee = new Employee();
account.setEmployee(employee);
Man kann diese beiden Entitäten persistieren und auch wieder aus der Datenbank laden
-
allerdings
wird
man
feststellen,
dass
danach
ein
Aufruf
von
account.getEmployee() null zurückgibt. Das liegt daran, dass die Entität
Employee nie von der gewünschten Assoziation zwischen den beiden Entitäten
erfahren hat, obwohl sie deren Eigentümerin ist. Verwendet man statt dessen
employee.setAccount(account);
so gelangt der Fremdschlüssel der Account-Instanz korrekt in die Datenbanktabelle
der Employee-Instanz und die Assoziation ist auch nach dem Persistieren und
erneutem Laden noch vorhanden.
Während bei einer bidirektionalen @OneToOne- oder @ManyToMany-Beziehung
mappedBy auf einer beliebigen Seite vorkommen kann, fordert die JPA im Falle einer
bidirektionalen @OneToMany-Assoziation, dass die Entität auf der Many-Seite der
Beziehung immer die Eigentümerin sein muss [4, S. 460]. Das bedeutet, dass hinter
@ManyToOne nie das Annotationselement mappedBy stehen darf. Das macht Sinn,
wenn man sich das entsprechende Datenbankschema vor Augen führt, und vereinfacht
zudem den Aufbau der Assoziationstests für @...ToOne.
Abbildung 3.7 stellt den schematischen Ablauf dar, welcher sich aus den obigen
Überlegungen für die @...ToOne-Tests ergibt. Dabei ist festzustellen, dass sich das
Vorhandensein von mappedBy lediglich auf den Testschritt auswirkt, in welchem des
Assoziationsattribut gesetzt wird (im Schema grau unterlegt). Dieser unterteilt sich nun
in zwei Varianten: Ohne mappedBy wird das Attribut der Test-Entität selbst gesetzt
20
(“direkt”), mit mappedBy statt dessen das betreffende Attribut der referenzierten
Entität (“invers”).
Abbildung 3.7: Schematischer Testablauf für @...ToOne-Tests
Bei den Tests für @OneToMany und @ManyToMany ergeben sich unter
Berücksichtigung von mappedBy größere Veränderungen. Während man im direkten
Fall (also ohne mappedBy) eine Collection oder Map für die referenzierten Instanzen
erstellen muss, entfällt das im inversen Fall (d. h. wenn die Test-Entität nicht die
Eigentümerin der Beziehung ist). Dann wird statt dessen jeweils das entsprechende
Attribut der beiden referenzierten Entitäten gesetzt. Bei @ManyToMany mit
mappedBy ist dann allerdings wieder eine Container-Instanz nötig, welche die
Test-Instanz selbst beinhalten muss.
Die Abbildung 3.8 verdeutlicht die drei alternativen Abläufe für die @...ToMany-Tests.
Man beachte, dass mappedBy auch hier nur Auswirkungen auf den Testschritt hat, in
21
dem das Assoziationsattribut gesetzt wird; alle anderen Testschritte (Erzeugen der
Instanzen, Persistieren, Laden) bleiben unverändert.
Abbildung 3.8: Schematischer Testablauf für @...ToMany-Tests
22
4 Implementierung
Nachdem im vorigen Kapitel die schematische Vorgehensweise für die zu erzeugenden
Tests erarbeitet wurde, soll nun deren Realisierung dargestellt werden. Im Unterkapitel
4.1 erläutere ich dazu die grundlegende Struktur der erzeugten Dateien, während im
Unterkapitel 4.2 die wichtigsten Elemente der konkreten Implementierung beschrieben
werden.
4.1 Struktur der erzeugten Dateien
In diesem Unterkapitel möchte ich auf den strukturellen Aufbau der generierten Tests
eingehen. Dabei unterscheide ich zwischen den eigentlichen Testklassen, welche die
Testfälle für die einzelnen Annotationen beinhalten, und einer Hilfsklasse, welche
Utility-Methoden zur Verfügung stellt. Der folgende Abschnitt (4.1.1) widmet sich dem
Aufbau der Testklassen, während danach kurz die Struktur der Hilfsklasse TestUtils.java
erläutert wird (4.1.2). Zum Abschluss wird das beschriebene Vorgehen anhand eines
Beispiels verdeutlicht (4.1.3).
4.1.1 Struktur der Testdateien
Um die Testdateien übersichtlich zu gestalten, bietet es sich an, jeweils eine Klasse mit
JUnit-Tests pro Entität anzulegen. Diese Klasse erhält ihren Namen nach dem Schema
<Entitätsname>Test
und
enthält
jeweils
einen
JUnit-Testfall
pro
ID-
oder
Assoziationsannotation. Da jede Entität zumindest eine ID besitzen muss, befindet sich
in der zugehörigen Testklasse also auch immer mindestens ein Testfall. Der Name der
Testfälle setzt sich zusammen aus dem Präfix „test“ gefolgt vom Namen des
betreffenden Attributs bei den Assoziationstests bzw. „Id“ bei den ID-Tests.
Die im vorigen Kapitel beschriebenen Testabläufe erfordern immer wieder den Zugriff
auf einzelne Attribute einer Entität, sei es zum Setzen einer ID, zum Herstellen einer
Assoziation oder zum Auslesen eines automatisch generierten Primärschlüssels.
Deshalb wird in den folgenden Abschnitten immer wieder der Zugriff auf einzelne
Attribute einer Entität erwähnt werden, welcher, da laut JPA-Spezifikation weder die
23
Felder noch die Properties einer Entität öffentlich sein müssen [4, S. 24], immer über
Reflection erfolgt. Mein Plugin unterstützt dabei sowohl Field als auch Property access
(vgl. Unterkapitel 2.1), indem es beim Setzen oder Auslesen von Attributen entweder
direkt
auf
die
betreffende
Instanzvariable
zugreift
oder
die
zugehörige
Accessor-Methode benutzt.
Aus den Ausführungen im vorigen Kapitel wurde auch ersichtlich, dass die Erzeugung
von Instanzen und das Setzen ihrer Primärschlüssel für jeden einzelnen Testfall
vonnöten sind. Damit man den zugehörigen Code nicht ständig wiederholen muss, liegt
es nahe, ihn in eigene Methoden auszulagern. Da man diese in jeder Testklasse benötigt,
werden sie als statische Methoden von einer Utility-Klasse mit dem Namen
TestUtils.java zur Verfügung gestellt (siehe folgenden Abschnitt). Dort werden die
Primärschlüssel für Entitäten mit einfachen und eingebetteten IDs sowie mit IdClass
gesetzt; bei einem automatisch generiertem Primärschlüssel muss man allerdings anders
vorgehen.
Eine automatisch generierte ID muss nicht gesetzt, sondern nur nach dem Setzen
ausgelesen werden, und das auch nur, wenn man sie zum Laden der zugehörigen Instanz
aus der Datenbank verwenden möchte. Das ist aber immer nur innerhalb der Testklasse
für die betreffende Entität selbst der Fall, welche deshalb eine private Methode enthält,
in welcher das ID-Attribut der Entität wie oben beschrieben je nach Zugriffsart über die
Instanzvariable oder die Getter-Methode unter Verwendung von Reflection ausgelesen.
Damit die folgenden Ausführungen leichter nachvollzogen werden können, habe ich im
Anhang das Listing 1 angefügt, welches den erzeugten Testfall für eine
@ManyToMany-Annotation (ohne mappedBy) exemplarisch darstellt.
Während das eigentliche Setzen oder Auslesen des Primärschlüssels in den ID-Tests
komplett in Hilfsmethoden ausgelagert ist, werden die benötigten Referenzen für die
Assoziationstests innerhalb der Testfälle selbst gesetzt. Hierbei wird zwischen den im
vorigen Kapitel beschriebenen Varianten unterschieden. Bei den @...ToOne-Tests wird
(je nach Vorhandensein von mappedBy) entweder das betreffende Attribut der
24
Test-Entität oder der referenzierten Entität gesetzt; auch in diesem Fall wird je nach
Field oder Property access mit Reflection auf das entsprechende Feld oder die
entsprechende Getter-Methode zugegriffen. Analog gibt es die @...ToMany-Tests in drei
verschiedenen Ausprägungen, die den im vorigen Kapitel beschriebenen möglichen
Varianten entsprechen (Listing 1, 16-23). Der Einfachheit halber werden bei diesen
Tests immer zwei Instanzen der referenzierten Entität erzeugt, um die korrekte
Funktionsweise der @OneToMany- und @ManyToMany-Assoziation zu überprüfen
(Listing 1, 12-14).
Um sicherzustellen, dass die Test-Entität tatsächlich in die Datenbank gelangt und auch
von dort (und nicht aus einem Puffer) geladen wird, werden für Speichern und
Wiederherstellen zwei verschiedene EntityManager-Instanzen genutzt [vgl. JPA, S. 63].
Im Persistenzkontext des ersten Entitätsmanagers wird eine Transaktion eröffnet, die im
Test erstellten Objekte persistiert, Transaction.commit() aufgerufen und die
EntityManager-Instanz geschlossen. Danach wird eine neue Instanz erstellt, eine neue
Transaktion begonnen und es wird versucht, die eben gespeicherte Test-Entität wieder
zu laden (Listing 1, 29-34).
Die Überprüfung der wiederhergestellten Entität ist je nach getesteter Annotation
verschieden umfangreich. Bei den ID-Tests wird lediglich getestet, ob man ein
konkretes Objekt zurückerhält oder eine null-Referenz. In den @...ToOne-Tests wird
zusätzlich der Wert des Assoziationsattributs per Reflection ausgelesen und auf null
überprüft. Bei den @...ToMany-Tests wird darüber hinaus noch erwartet, dass die
erhaltene Collection genau zwei Einträge hat und dass diese Einträge beide nicht null
sind (Listing 1, 47-52).
Die Testklassen enthalten Testfälle für alle annotierten Attribute einer Entität, also auch
für solche, die von einer Superklasse geerbt werden. Die JPA-Spezifikation erlaubt
Vererbungsbeziehungen zwischen Entitäten, Nicht-Entitäten und MappedSuperclasses8,
welche abstrakt oder konkret sein können [4, S. 54]. Eine Entität erbt dabei immer
8
Wenn eine Klasse zwar Annotationen vererben, allerdings nicht in einer eigenen Tabelle abgebildet
werden soll, wird sie statt mit @Entity mit @MappedSuperClass annotiert.
25
sämtliche Annotationen ihrer Superklasse. Im Falle einer ererbten ID wird die in der
TestUtils-Klasse vorhandene Methode zum Setzen der ID der entsprechenden
Superklasse aufgerufen. Als Parameter bekommt sie die Instanz der Test-Entität
(Subklasse) übergeben. Das ist kein Problem, da innerhalb dieser Methode das
Superklassen-Attribut per Reflection für die Subklassen-Instanz gesetzt werden kann.
Analog wird bei der Vererbung von Assoziationen vorgegangen: Hier wird das zu
testende Attribut der Superklasse für die Testinstanz gesetzt.
4.1.2 Struktur der Hilfsklasse TestUtils.java
Die im vorigen Abschnitt erwähnte Klasse TestUtils.java stellt mit ihren
Utility-Methoden die Grundlage für alle Tests zur Verfügung. Sie beinhaltet zuallererst
eine statische Methode zum Erzeugen von Instanzen einer als Parameter übergebenen
Klasse. Da die JPA-Spezifikation erlaubt, dass der Konstruktor einer Entität
protected sein kann [4, S. 23], muss an dieser Stelle Reflection zum Aufruf des
Konstruktors genutzt werden. Da die Spezifikation aber ebenso das Vorhandensein eines
parameterlosen Konstruktors verlangt, ist der Aufbau dieser Methode recht simpel.
Desweiteren enthält TestUtils.java öffentliche statische Methoden zum Setzen der IDs
für alle Klassen (Entitäten oder MappedSuperClasses), die selbst einen (nicht
automatisch generierten) Primärschlüssel definieren. Diese Methoden erwarten als
Parameter eine Instanz der betreffenden Entität. Falls diese in einer @OneToManyoder @ManyToMany-Assoziation referenziert wird, wird zusätzlich eine Methode
erzeugt, welche zwei Instanzen mit Primärschlüsseln mit unterschiedlichen Werten
versorgt.
Das eigentliche Belegen der einzelnen Attribute einer Entität mit konkreten Werten
findet dann in privaten Methoden statt, welche als Parameter eine Instanz der Entität
und den zu setzenden Wert erwarten. Auch hier wird entsprechend der Zugriffsart unter
Verwendung von Reflection entweder das betreffende Feld direkt gesetzt oder die
zugehörige Setter-Methode aufgerufen.
26
4.1.3 Beispiel
Das eben beschriebene Vorgehen soll im Folgendem an einem kleinen konkreten
Beispiel verdeutlicht werden. Gegeben seien folgende Klassen:
@Entity
@IdClass(EmplId.class)
public class Employee {
@Id
private Long id;
@Id
private String branch;
@OneToOne
private SystemAccount account;
@ManyToMany(mappedBy = "skilled")
private Set<Skill> skills;
...
}
public class
private
private
private
...
}
EmplId implements Serializable{
static final long serialVersionUID = 1L;
Long id;
String branch;
@Entity
public class SystemAccount {
@Id
@GeneratedValue
private int uid;
@OneToOne(mappedBy = "account")
private Employee employee;
...
}
@Entity
public class Skill {
@EmbeddedId
private SkillDetails id;
27
@ManyToMany
private Set<Employee> skilled;
...
}
@Embeddable
public class SkillDetails implements Serializable{
private static final long serialVersionUID = 1L;
private String key;
private Date createdDate;
...
}
Wie man sieht, verwendet die Entität Employee einen zusammengesetzten
Primärschlüssel mit @IdClass (die entsprechende ID-Klasse ist EmplId), die Entität
Skill enthält eine eingebettete ID vom Typ SkillDetails, während der
Primärschlüssel der Entität SystemAccount per @GeneratedValue automatisch
generiert wird. Damit ist außer einem einfachen Primärschlüssel jede der im vorigen
Kapitel beschriebenen ID-Arten vertreten sowie eine unidirektionale @OneToOne- und
eine bidirektionale @ManyToMany-Assoziation.
Abbildung 4.1 zeigt die Klassendiagramme der daraus erzeugten Klassen. In den drei
Testklassen wurde jeweils ein Test für das ID-Attribut erstellt sowie ein Test für jede
Assoziationsannotation. Das im Abschnitt 4.1.2 verwendete Listing 1 (s. Anhang)
enthält den Testfall, der für das mit @ManyToMany annotierte Attribut skilled der
Entität Skill erzeugt wurde. Die Entität SystemAccount enthält keine
Assoziationsannotationen,
deshalb
besitzt
die
zugehörige
Testklasse
SystemAccountTest.java lediglich einen Testfall für den Primärschlüssel. Da dieser aber
automatisch
generiert
wird,
gibt
es
dort
zusätzlich
die
private
Methode
getGeneratedIdValue() zum Auslesen des konkreten Wertes.
Die Hilfsklasse TestUtils.java enthält die Methode createInstance() zur
Objekterzeugung sowie Methoden zum Setzen der IDs der Entitäten Skill und
Employee. (SystemAccount kommt dort nicht vor, weil es eben einen automatisch
generierten Primärschlüssel hat.) Da beide Entitäten in @...ToMany-Assoziationen
28
referenziert werden, wurden für beide setIdFor...Twice-Methoden erstellt, damit in den
betreffenden
Testfällen
(EmployeeTest.testSkills()
und
SkillTest.testSkilled()) jeweils zwei Instanzen mit IDs versorgt werden
können. Die restlichen Methoden setzen einzelne ID-Felder der Entität oder der
zugehörigen ID-Klasse.
Abbildung 4.1: Klassendiagramm der erzeugten Klassen
29
4.2 Implementierung mit Acceleo
Mein Programm zur Erzeugung der Annotationstests ist als Eclipse-Plugin
implementiert, passend zu den anderen Plugins des Dr. Deepfix-Projektes. Um es zu
starten, muss man per Rechtsklick ein Java-Projekt im Eclipse Project Explorer
auswählen und im erscheinenden Menü „Dr. Deepfix (Utilities)“ -> „Generate Tests“
auswählen.
Daraufhin wird der Plugin-Code ausgeführt und erhält als Input ein Ecore-Modell des
ausgewählten Projektes. Ecore ist ein Format für Datenmodelle, welches die Grundlage
des Eclipse Modeling Framework (EMF) bildet. Das Eingabemodell meines Plugins
enthält alle Elemente des Java-Projektes mit zusätzlichen Attributen, welche Auskunft
geben über etwaig vorhandene JPA-Annotationen. Aus diesem Modell werden die
Dateien für die JUnit-Tests unter Verwendung von Acceleo erzeugt, das im folgenden
Abschnitt beschrieben ist. Der Abschnitt 4.2.2 geht dann genauer auf den Ablauf der
Code-Erzeugung ein.
4.2.1 Einführug in Acceleo
Acceleo ist ein Tool zur Generierung von Code aus EMF-kompatiblen Modellen [5]. Es
implementiert den MOFM2T-Standard der Object Management Group und wird
inzwischen von der Eclipse Foundation entwickelt. Die Codegenerierung erfolgt auf
Basis von Templates, welche neben dem auszugebenden Text auch Platzhalter enthalten,
deren Wert von Elementen des Eingabemodells bestimmt wird. Diese Ausdrücke folgen
der Eclipse-Implementierung der Object Constraint Language (OCL).9
Der Code in einem Acceleo-Projekt besteht aus Dateien mit der Endung .mtl, welche
sogenannte
Module
darstellen.
Ein
Modul
kann
sowohl
Templates
(zur
Codegenerierung) als auch Queries (zum Kapseln komplexer Ausdrücke) enthalten. Im
Kopf jeden Moduls muss die URI des zugehörigen Meta-Modells angegeben werden.
Außerdem können dort andere Acceleo-Module importiert werden, um auf deren
öffentliche Elemente (also Templates oder Queries) zuzugreifen, für welche Acceleo die
9
Eine gute Übersicht über Acceleo findet sich unter [6].
30
drei Access modifier public, protected und private anbietet. Sie regeln die
Sichtbarkeit von Elementen ähnlich wie in Java, hier jedoch zwischen Modulen statt
zwischen Klassen. Wie das Vorhandensein des Modifikators protected vermuten
lässt, ermöglicht Acceleo auch Vererbungsbeziehungen zwischen Modulen.
Der folgende Code zeigt als Beispiel ein sehr einfach aufgebautes Acceleo-Modul
namens myModule.mtl, welches sowohl ein Template als auch eine Query enthält. Der
Name des öffentlichen Templates (genInfo) folgt dabei der Acceleo-Namenskonvention,
wonach öffentliche Templates mit dem Präfix „gen“ und öffentliche Queries mit „req“
zu markieren sind [6, Abschnitt Generating Files]. Die private Query im Beispiel
demonstriert den hauptsächlichen Verwendungszweck dieser Elemente: Zum Aufrufen
einer Methode einer Java-Assistenzklasse, welche kompliziertere Operationen als die
beschränkte Syntax von Acceleo ermöglicht. Queries werden von Acceleo aus
Performanzgründen gecacht, so dass der Aufruf einer Query für ein und denselben
Eingabeparameter immer nur einmal ausgeführt wird [6, Abschnitt Query].
[module myModule('http://feu.de/ps/java')]
[template public genInfo(aClassDecl : TypeDeclaration) post(trim())
{className: String = aClassDecl.name;}]
Das geerbte ID-Feld der Klasse [className/]
heisst [aClassDecl.getInheritedIdField().name/].
[/template]
[query private getInheritedIdField (typeDecl : TypeDeclaration) :
FieldDeclaration =
invoke('de.feu.ps.jee.lang.jpa.testgenerator.acceleo.common.Service
',
'getInheritedIdField(de.feu.ps.modisco.lang.jee3.model.jee.java.Typ
eDeclaration)', Sequence{typeDecl})
/]
Neben Vorbedingungen („Guards“), die die Ausführung des Templates von dem
Ergebnis
eines
booleschen
Ausdrucks
abhängig
machen,
können
auch
Nachbedingungen angegeben werden, welche nach der Ausführung des Templates
ablaufen. Im obigen Beispiel wird letzteres genutzt, um alle den Ausgabetext
einschließenden Whitespace-Zeichen zu entfernen (trim()). Desweiteren können im
Kopf eines Templates lokale Variablen definiert werden; auch dies wird im Beispiel
exemplarisch verwendet (className).
31
In Templates können durch Verwendung des Schlüsselwortes file Dateien erzeugt
werden, wobei der gewünschte Dateiname, die Encodierung und das Verhalten bei
bereits vorhandener Datei (Anhängen oder Überschreiben) anzugeben ist. Der gesamte
Ausgabetext, der innerhalb des file-Blocks erzeugt wird, erscheint dann in der
entsprechenden Datei.
Die oben erwähnte spartanische Syntax von Acceleo umfasst im wesentlichen
konditionale Ausdrücke mit if, else und else if, Schleifen mit for und
Variablendefinitionen mit let, elselet und else. Letztere weisen nicht nur Werte
zu, sondern folgen der MOFM2T-Spezifikation und haben damit dieselbe Funktion wie
das Java-Äquivalent if (x instanceof Type) { Type var = (Type)x; ...}
[9, S. 6]. Die Verwendung von let ist allerdings recht umständlich, wenn man mehr als
eine Variable definieren möchte; meist bietet sich hier eher die Verwendung eines
privaten Templates an, in dessen Kopf die nötigen Variablen festgelegt werden können.
4.2.2 Ablauf der Codegenerierung
In diesem Abschnitt möchte ich den tatsächlichen Ablauf der Codeerzeugung grob
skizzieren und die wichtigsten Elemente meiner Implementierung kurz vorstellen.
Abbildung 4.4 gibt dazu einen Überblick über die von mir erstellten Acceleo-Module
sowie ihr Zusammenspiel und ihre Verwandtschaftsbeziehungen. Die meisten Module
beinhalten dabei ein öffentliches Template als Einstiegspunkt sowie ggf. ein oder
mehrere private Templates oder Queries. Eine Ausnahme davon stellen lediglich die
zwei Module generateTest.mtl und queries.mtl dar, auf die ich später noch eingehen
werde.
32
Abbildung 4.4: Aufrufabhängigkeiten und Vererbungsbeziehungen zwischen den Acceleo-Modulen
Den Einstiegspunkt für meinen Code stellt die Klasse InvokeCreateTestHandler.java
dar, welche beim Ausführen des Plugins aufgerufen wird. Dort wird der Source folder
test-gen angelegt (falls er nicht bereits existiert) und zum Classpath des Projektes
hinzugefügt. Er soll später die erzeugten Tests beherbergen. Desweiteren iteriere ich
über alle Objekte des übergebenen Ecore-Modells, um herauszufinden, ob dafür
überhaupt Tests erzeugt werden können - d. h. ob das ausgewählte Projekt Entitäten
enthält. Sollte das der Fall sein, beginnt die Ausführung des Acceleo-Codes im
main-Template meines Plugins. Dort werden die beiden Module junitJavaFiles.mtl und
utilJavaFile.mtl aufgerufen.
Das Modul utilJavaFile.mtl ist für die einmalige Generierung der Klasse TestUtils.java
zuständig. Die in diesem Modul enthaltenen Templates erstellen zuerst den Rahmen der
Klasse
mit
den
benötigten
Import-Angaben.
Danach
wird
die
Methode
createInstance() erzeugt, gefolgt von den öffentlichen und privaten Methoden
zum Setzen von IDs. Dabei wird über alle vorhandenen Entitäten iteriert und für solche
mit einem lokalen (also nicht geerbten) und nicht automatisch erzeugten
Primärschlüssel die benötigten Methoden erzeugt, je nachdem, über welche Art von ID
33
(einfach, @IdClass, @EmbeddedId) die Entität verfügt. Bei Verwendung von
@IdClass und
@EmbeddedId müssen entsprechende Methoden jeweils noch
zusätzlich für die ID-Klasse erstellt werden.
Beim Belegen der einzelnen ID-Attribute mit Default-Werten muss bedacht werden,
dass die JPA-Spezifikation [4, S. 30] folgende Datentypen für einfache Primärschlüssel
oder einzelne Teile von zusammengesetzten Primärschlüsseln erlaubt:
•
alle primitiven Datentypen
•
alle Wrapper für primitive Datentypen
•
java.lang.String,
java.util.Date,
java.sql.Date,
java.math.BigDecimal und java.math.BigInteger
Die Generierung eines passenden Default-Wertes geschieht aufgrund ihrer Komplexität
in der Assistenzklasse Service.java. Dort wird für primitive Datentypen anhand ihres
Namens ein geeigneter Wert vergeben, im Falle von Wrappern wird eine neue Instanz
erzeugt, die mit dem Default-Wert des entsprechenden primitiven Datentypen
initialisiert wird, und für die verbleibenden komplexeren Datentypen neue Instanzen mit
passenden Parametern erzeugt. Ich habe für alle numerischen Datentypen die Zahl 4 als
Default-Wert genommen.10 Die Verwendung eines zufällig generierten Wertes ist hier
unnötig, da die Tests jeweils auf Basis einer frischen Datenbank durchgeführt werden,
also noch keine Einträge mit anderen IDs vorhanden sind. Der Wert 1 sollte als Default
jedoch vermieden werden, da es u. U. passieren kann, dass genau dieser Wert (eben
aufgrund der leeren Datenbank) vom Persistenz-Provider bei einer ID mit
@GeneratedValue vergeben wird. Das könnte dazu führen, dass bei einem
fälschlicherweise hinzugefügten @GeneratedValue der betreffende Testfall, der
eigentlich scheitern müsste, problemlos durchläuft, weil sowohl manuell im Testfall als
auch automatisch vom Persistenzprovider zufälligerweise derselbe ID-Wert gesetzt
wird.
10 Vgl. http://xkcd.com/221.
34
Im Modul junitJavaFiles.mtl wird für jede Entität eine Testdatei mit dem Namen
<Entitätsname>Test.java samt ihrem Grundgerüst erstellt. Dazu gehören zum einen die
benötigten Importe, zum anderen aber auch eine Liste aller zum Ausführen der Tests
erforderlichen
Entitäten,
welche
den
Rückgabewert
der
Methode
getAnnotatedClasses() bilden. Letztere ist nötig, da ich meine Tests von der
Klasse BaseEntityManagerFunctionalTestCase ableite, welche in den
JUnit-Tests des Hibernate-Projektes selbst verwendet wird und das Erstellen von
Testfällen auf Basis von Hibernate stark vereinfacht.
Innerhalb des Grundgerüsts für die Testklassen wird anschließend der ID-Test für die
Entität erstellt, was im Acceleo-Modul generateIdTest.mtl geschieht. Danach werden für
alle Annotationsattribute der Entität (inklusive der geerbten) Testfälle erzeugt; je nach
Kardinalität
wird
dafür
generateToOneTest.mtl
oder
generateToManyTest.mtl
aufgerufen. Diese drei Module zur Erzeugung der konkreten Testfälle erben alle vom
Modul generateTest.mtl, welches die für jeden Testfall benötigten Templates zum
Generieren der korrekten Methodenaufrufe für die Instanzerzeugung, das Setzen oder
Auslesen der IDs oder das Laden von Entitäten bereithält.
Während sich in diesem Fall durch die großen Gemeinsamkeiten der drei Module die
Einführung eines gemeinsamen Elternmoduls anbot, ließ sich bei queries.mtl leider
keine sinnvolle Verwandtschaftsbeziehung konstruieren. Dieses Utility-Modul enthält
eine kleine Sammlung öffentlicher Queries, die Methoden der Assistenzklasse aufrufen
und
jeweils
von
mehreren
anderen
Modulen
genutzt
werden,
z.
B.
reqHasGeneratedId (TypeDeclaration) : Boolean. (Vgl. Abb. 4.4)
Wie bereits in Abschnitt 4.1.1 beschrieben, erfolgt das Setzen und Auslesen von
JPA-Attributen in den erzeugten Testklassen und in der Hilfsklasse TestUtils durch
Reflection. Dabei muss jedoch die gewählte Zugriffsart beachtet werden: Bei Field
access muss auf die betreffende Instanzvariable zugegriffen, bei Property access die
passende Setter- oder Getter-Methode aufgerufen werden. Konkret benötige ich dies
zum Setzen der einzelnen ID-Attribute (utilJavaFile.mtl), zum Auslesen des automatisch
generierten Primärschlüssels (junitJavaFiles.mtl) und zum Herstellen und Prüfen von
35
Assoziationen (generateToOneTest.mtl, generateToManyTest.mtl). Damit ich die
genannten Templates nicht doppelt vorhalten muss (eine Version für Field access, eine
für Property access), gehe ich folgendermaßen vor:
Wenn
ich
innerhalb
einer
Entität
nach
deren
Primärschlüssel
oder
Assoziationsannotationen suche, gehe ich sowohl Feld- als auch Methodendeklarationen
durch und gebe das Ergebnis in Form des Supertyps MemberDeclaration 11 zurück.
Dieser stellt mir größtenteils dieselben Methoden wie seine beiden Untertypen zur
Verfügung,
so
dass
ich
problemlos
in
meinen
Acceleo-Templates
MemberDeclaration verwenden kann, um z. B. den referenzierten Typ oder
bestimmte JPA-Eigenschaften einer Instanzvariable oder Methode abzufragen.
Wenn ich nun aber Code erzeugen will, der per Reflection auf einen bestimmten
Member zugreift, dann muss ich wissen, ob es sich um eine Instanzvariable oder eine
Methode handelt, da sich der erzeugte Code hierfür unterscheidet. Dazu bietet sich in
Acceleo der let-Ausdruck an, der - wie bereits in Abschnitt 4.2.1 beschrieben - eine
ähnliche Semantik wie instanceof in Java hat. Zur Illustration habe ich das folgende
Listing eingefügt, welches ein Template zeigt, das die privaten Methoden der Klasse
TestUtils zum Setzen einzelner ID-Attribute erzeugt und im Modul utilJavaFile.mtl
liegt.
1
[template private generateSetMember(aType : TypeDeclaration, aMember :
2
MemberDeclaration) post(trim())
3
4
{memberType : TypeDeclaration = aMember.referredType.referredCommon;
memberName : String = aMember.reqGetName();}]
5
6
private static void ['set'.concat(aType.name).concat(memberName.toUpperFirst())/]
7
([aType.name/] [aType.name.toLowerFirst()/], [memberType.name/]
8
[memberName.toLowerFirst()/]) throws Exception {
9
10
11
[let aField : FieldDeclaration = aMember]
Field field = [aType.name/].class.getDeclaredField("[aField.name/]");
11 de.feu.ps.modisco.lang.jee3.model.jee.java.MemberDeclaration
36
12
field.setAccessible(true);
13
field.set([aType.name.toLowerFirst()/], [memberName.toLowerFirst()/]);
14
field.setAccessible(false);
15
[elselet aMethod : MethodDeclaration = aMember]
16
Method method =
17
[aType.name/].class.getDeclaredMethod("[aMethod.jpaSetter.name/]",
18
[memberType.name/].class);
19
method.setAccessible(true);
20
method.invoke([aType.name.toLowerFirst()/], [memberName.toLowerFirst()/]);
21
method.setAccessible(false);
22
[/let]
23
}
24
[/template]
Das
Template
benötigt
als
Inputparameter
zum
einen
natürlich
die
MemberDeclaration selbst, zum anderen auch die TypeDeclaration des Typs,
welcher den Member deklariert. Im Kopf des Templates werden Variablen für Name
und Typ des Members definiert (Z. 3-4), danach wird unter Verwendung dieser
Variablen zuerst einmal der Kopf der Methode generiert (Z. 6-8). 12 Im folgenden
let-Block wird dann je nach konkretem Typ des Members verschiedener Code erzeugt.
Falls es sich um eine FieldDeclaration handelt, werden die Zeilen 11-14
ausgeführt und generiert, falls es sich um eine MethodDeclaration handelt, die
Zeilen 16-21.13
Die JPA-Spezifikation verlangt die Verwendung einer einheitlichen Zugriffsart für die
gesamte Entitätshierarchie [4, S. 27]. Ausnahmen davon können mit der
@Access-Notation gekennzeichnet werden [4, S. 422]. Da ich in meinem Code aber
jeweils die einzelnen annotierten Attribute betrachte, erzeuge ich unabhängig von Field
oder Property access immer Tests für alle ID- und Assoziationsannotationen.
12 Der dann zum Beispiel so aussieht:
private static void setEmployeeBranch(Employee employee, String branch) throws
Exception {
13 Ein anderer Subtyp von Member kann an dieser Stelle nicht auftauchen, da ich schon im Vorfeld nur
Instanzvariablen und Methoden der Entitätsklassen auf JPA-Annotationen hin untersuche.
37
5 Ergebnisse
Nach der Entwicklung der benötigten Testschemata und ihrer Implementierung stellt
sich natürlich die Frage, inwieweit sich mein Ansatz als zielführend erweist. Erfüllen
die von meinem Plugin generierten Tests tatsächlich ihren Zweck und erkennen
semantische Änderungen an JPA-Annotationen? Im nächsten Unterkapitel (5.1) lege ich
mein Vorgehen dar, um das zu evaluieren, sowie die Ergebnisse, die ich dabei erhalten
habe. Eine Diskussion derselben schließt sich in Unterkapitel 5.2 an.
5.1 Evaluation
Um die Effektivität der erzeugten Testfälle beurteilen zu können, habe ich sie sowohl
automatisiert als auch manuell überprüft. Für ersteres konnte ich ein von Bastian Ulke
im Rahmen von Dr. Deepfix entwickeltes Tool benutzen, welches Permutationen von
Projekten erzeugt, indem es Annotationen hinzufügt oder entfernt, und dann jeweils die
vorhandenen Tests ausführt und ihre Ergebnisse protokolliert. Dabei wurden die
Annotationen @Id und @EmbeddedId vom ursprünglichen Attribut entfernt und an
ein oder mehrere andere Attribute derselben Entität hinzugefügt sowie das
Annotationselement mappedBy entfernt oder sein Wert durch ein anderes Feld der
betreffenden Entität ersetzt. Im Zuge dessen wurden jeweils alle möglichen
Kombinationen durchlaufen.
Bei meinen manuellen Tests habe ich systematisch z.B. die Kardinalitäten von
Assoziationsannotationen geändert, @GeneratedValue hinzugefügt oder entfernt
oder die Art der ID geändert. Wenn es durch die große Zahl der möglichen
Kombinationen auch unrealistisch ist, alles abzutesten, habe ich durch meine
Stichproben in Verbindung mit der automatisierten Evaluation doch einen einigermaßen
guten Überblick über die Angemessenheit der generierten Tests erhalten. Dabei traten
tatsächlich auch einige Schwächen zutage.
Man kann die Testläufe nach ihrem Ausgang dabei grob in drei große Gruppen
unterteilen:
1. Error: Die Tests brachen mit einer Hibernate-Fehlermeldung ab.
38
2. Failure: Die Tests schlugen aufgrund von nicht erfüllten JUnit-Assertions fehl.
3. Success: Die Tests liefen durch.
Die weitaus meisten Testdurchläufe gehören zur ersten Gruppe. Das ist auch nicht
verwunderlich, da die vorgenommenen Mutationen meist die Konsistenz des
JPA-Projektes zerstören, was dem Persistenzprovider im Laufe seiner Arbeit früher oder
später auffallen sollte. Dieses “früher oder später” zeigt sich an den drei verschiedenen
Stellen, an denen die Hibernate-Fehlermeldungen auftraten.
In der allergrößten Mehrheit der Fälle brachen die Tests schon beim Start von Hibernate
ab, weil der Aufbau der benötigten EntityManagerFactory nicht möglich war [4,
S. 338]. Dies geschah zum Beispiel, wenn die Entität keinerlei ID besaß oder bei
mappedBy der Name eines nicht existenten Feldes angegeben worden war. Meist
wurde von Hibernate eine entsprechende Fehlermeldung erzeugt; in einigen Fällen
brach der Test allerdings auch mit einer NullPointerException ab.
Zuweilen scheiterte Hibernate auch erst beim EntityManager.persist(), also
beim Speichern der Entitäten im Persistenzkontext. Dies war immer dann der Fall, wenn
die vorhandenen Annotationen zwar in sich konsistent waren, allerdings nicht mehr zu
den vorher erstellten Testfällen passten. Eine Entität konnte zum Beispiel nicht mehr
persistiert werden, wenn (bei Verwendung eines einfachen Primärschlüssels) die
Annotation @Id an ein anderes ihrer Attribute verschoben worden war. Da der vorher
erzeugte Testfall lediglich das ehemalige ID-Attribut setzte und die anderen Attribute
initial beließ, blieb der Primärschlüssel einer solchen Mutation leer, was dazu führte,
dass sie nicht in der Datenbank abgelegt werden konnte.
Auch das Entfernen der Annotation @GeneratedValue und damit Umwandeln eines
automatisch generierten in einen einfachen Primärschlüssel führte zu diesem Ergebnis,
da dann im zugehörigen Testfall das entsprechende ID-Attribut gar nicht gesetzt wurde.
Überraschenderweise erzeugte Hibernate sogar im umgekehrten Fall - wenn
@GeneratedValue also hinzugefügt und damit im Testverlauf das ID-Feld zweimal
gesetzt wurde - eine Fehlermeldung beim persist(). Das ist darin begründet, dass
39
Hibernate eine Entität, die bisher nicht im Persistenzkontext vorhanden ist und deren ID
automatisch generiert werden soll, die aber beim persist() trotzdem schon einen
Primärschlüssel-Wert hat, für losgelöst vom Persistenzkontext (“detached”) hält und
deshalb
beim
Versuch,
diese
mit
EntityManager.persist()
statt
EntityManager.merge() in die Datenbank zu übertragen, streikt.
Die dritte Stelle, an der Hibernate-Fehlermeldungen auftraten, wenn auch ebenfalls sehr
selten, war beim EntityTransaction.commit(). Das war zum Beispiel der Fall,
wenn beim Annotationselement mappedBy ein falscher Feldname angegeben worden
war oder wenn im Fall eines zusammengesetzten Primärschlüssels mit @IdClass
Assoziationsattribute zusätzlich mit @Id markiert worden waren.
Verglichen mit der großen Anzahl der Fälle, in denen die erzeugten Tests durch
Hibernate-Fehlermeldungen abgebrochen wurden, sind eher wenige aufgrund ihrer
Assertions fehlgeschlagen. Dies trat vor allem im Zusammenhang mit mappedBy auf.
Wie in Abschnitt 3.3.2 geschildert, wird durch Einfügen oder Auslassen des
Annotationselements mappedBy angezeigt, von welcher der beiden beteiligten
Entitäten eine bidirektionale Assoziation verwaltet wird. Dementsprechend muss das
Assoziationsattribut dieser Entität gesetzt werden: Bei fehlendem mappedBy ist das
die Test-Entität selbst, bei vorhandenem mappedBy die referenzierte Entität. Nur dann
kann die Assoziation korrekt in die Datenbank gelangen.
Wenn nun aber ein vorhandenes mappedBy gelöscht oder ans andere Ende der
Assoziation verschoben wird, dann führt das dazu, dass in den vorher erstellten Tests
das Attribut der Nicht-Eigentümer-Entität gesetzt wird. Damit können zwar die
Entitäten problemlos in der Datenbank gespeichert werden, allerdings kann Hibernate
beim Wiederherstellen aus der Datenbank die assoziierte Entität nicht finden, da es
durch das verschobene/fehlende mappedBy in der Tabelle der falschen Entität
nachschaut.
Dadurch
schlagen
die Assertions
fehl,
welche
im
Laufe
der
Assoziationstests überprüfen, ob die von der Test-Entität (welche sich problemlos laden
40
lässt) referenzierte Entität null ist (bei den @...ToOne-Tests) bzw. die Liste der
referenzierten Entitäten keine Einträge enthält (bei den @...ToMany-Tests).
Nach den Überlegungen in Kapitel 3 müssten die von meinem Plugin generierten Tests
eigentlich immer fehlschlagen, sobald sinnentstellende Veränderungen an den
vorhandenen Annotationen vorgenommen werden. Dass dies aber offensichtlich nicht
durchgängig der Fall ist, beweisen die “erfolgreichen” Testdurchläufe, in denen trotz
geänderter Annotationen weder eine Hibernate-Fehlermeldung auftrat noch eine
Assertion nicht erfüllt wurde.
Dieses Verhalten konnte bei genau zwei Fällen beobachtet werden. Zum einen, wenn die
erste Komponente einer Assoziationsannotation geändert wurde, also @OneToOne
durch @ManyToOne ersetzt oder @OneToMany durch @ManyToMany und vice versa.
In den generierten Testfällen werden immer nur die von der betreffenden Entität
ausgehenden Assoziationen getestet; die Rückrichtung findet keine Beachtung. Da
Hibernate diese bei der Validierung eines JPA-Projektes ebenfalls zu ignorieren
scheint14, laufen die erzeugten Tests durch, obwohl die Annotationen nicht mehr
konsistent sind.
Der zweite Fall, in dem veränderte Annotationen die Tests passierten, obwohl diese
eigentlich hätten scheitern müssen, trat im Zusammenhang mit der Verwendung von
@IdClass auf. Hibernate scheint dabei die @Id-Annotationen in der Entität selbst
weitestgehend zu ignorieren und lediglich die Annotationen in der zugehörigen
ID-Klasse zu beachten. So konnten in der betreffenden Entität @Id-Annotationen
entfernt oder hinzugefügt werden, ohne dass das einen Einfluss auf die Testdurchläufe
hatte, solange mindestens ein (beliebiges) Attribut mit @Id markiert blieb. Es war sogar
möglich, ein Assoziationsattribut zusätzlich mit @Id zu annotieren, was sonst (also im
Falle einer einfachen ID, ohne Verwendung von @IdClass) bereits beim Aufbau der
EntityManagerFactory zu einer Hibernate-Fehlermeldung geführt hatte.
14 Allerdings überprüft Hibernate, ob mappedBy auf beiden Seiten einer bidirektionalen Assoziation
vorkommt. In diesem Fall erzeugt es eine Fehlermeldung.
41
Hibernate missachtet bei der Validierung von Entitäten, die @IdClass verwenden,
offensichtlich die Vorgaben der JPA-Spezifikation, welche ausdrücklich verlangt, dass
Name und Typ der ID-Attribute in der Entität selbst und in ihrer ID-Klasse
übereinstimmen [4, S. 30, S. 449]. Ich konnte keine offizielle Erklärung für dieses
Verhalten finden, allerdings wird in der Hibernate-Dokumentation ausdrücklich vor der
Verwendung von @IdClass gewarnt: “This approach is inherited from the EJB 2 days
and we recommend against its use. But, after all it's your application and Hibernate
supports it.” [12, 2.2.3.2.3]
5.2 Diskussion
Zusammenfassend lässt sich sagen, dass die von mir konzipierten Tests in den meisten
Fällen wie erwartet funktionieren, also semantische Veränderungen an JPA-Metadaten
aufdecken. Dabei werden die meisten Fehler bereits von Hibernate erkannt und
angezeigt, noch ehe der Test bei seinen eigentlichen Assertions angelangt ist. Einige
Fälle, die Hibernate entgehen, werden schließlich von den Assertions selbst entdeckt. Es
gibt aber leider auch Konstellationen, in denen die von meinem Plugin erzeugten Tests
beschwerdelos durchlaufen, obwohl verhaltensändernde Modifikationen an den
JPA-Annotationen vorgenommen wurden.
Um auch diese Durchläufer abzufangen, müssten die Tests grundlegend anders
konzipiert werden. Im oben beschriebenen ersten Fall, in dem die Kardinalität der
Rückreferenz einer Assoziationsannotation geändert wird (z. B. @ManyToOne zu
@OneToOne), genügt es zum Beispiel nicht, für bidirektionale Assoziationen noch
einen zusätzlichen Test der Rückrichtung hinzuzufügen. Dieser existiert nämlich bereits,
und zwar bei den für die referenzierte Entität erzeugten Testfällen. Wenn die
Kardinalität allerdings nur auf einer Seite der Annotation geändert wird, haben die Tests
keine Chance, dies aufzudecken. Dies wäre auch viel eher Aufgabe des
Persistenzproviders, da eine solche Veränderung eindeutig zu Inkonsistenzen innerhalb
des JPA-Projekts führt.
Im zweiten oben beschriebenen Fall laufen die generierten Tests durch, obwohl die
42
@Id-Annotationen in einer Entität, welche @IdClass nutzt, geändert wurden. Grund
dafür ist die nachlässige Validierung durch Hibernate in Bezug auf diese Annotation.
Um das zu umgehen, müsste allerdings ein ganz anderes Konzept für die Tests gefunden
werden. Eine weitere Alternative wäre natürlich, einen anderen Persistenzprovider zu
nutzen, der u. U. eine genauere Implementierung der JPA-Spezifikation bietet als
Hibernate.15
Wenn man den Aufbau dieser Abschlussarbeit betrachtet, stellt man fest, dass die
Problemstellung sich lediglich auf die JPA bezog: Es sollten Tests für bestimmte
Annotationen und Annotationselemente erzeugt werden. Im Kapitel 3 wurden dann
Schemata für den Aufbau der Tests für eben diese Metadaten entwickelt. Ausgangspunkt
war dabei ihre in der JPA-Spezifikation festgelegte Bedeutung. Im Kapitel 4 fand
Hibernate als Persistenzprovider Erwähnung, um dann plötzlich einen großen Teil des
Inhalts dieses 5. Kapitels zu bestimmen. Aber sollte der Inhalt dieser Arbeit nicht
eigentlich die Java Persistence API selbst sein?
Tatsächlich ist es so, dass man bei der Verwendung der JPA immer darauf angewiesen
ist, wie sie - als reine Schnittstellendefinition - vom Persistenzprovider tatsächlich
umgesetzt wird. Wie man schon im Rahmen der wenigen Annotationen, auf die in dieser
Arbeit eingegangen wurde, sieht, wird die Spezifikation nicht immer komplett
implementiert. Manchmal wird auch über die Spezifikation hinausgegangen, um den
Nutzer beim durchaus nicht trivialen Umgang mit der JPA zu unterstützen. Letzteres
trifft z.B. im oben beschriebenen Fall zu, in dem Hibernate eine Fehlermeldung erzeugt,
wenn es feststellt, dass ein ID-Attribut, für welches mit @GeneratedValue ein
Primärschlüssel automatisch erzeugt werden soll, bereits einen Wert besitzt.16
Manche Aspekte bleiben in der JPA-Spezifikation auch offen und sind damit ganz dem
Ermessen des Persistenzproviders überlassen. Dazu zählt z. B. die Frage, wann genau
15 Hier würde sich evtl. EclipseLink in seiner Eigenschaft als Referenzimplementierung der Java
Persistence API anbieten.
16 Dadurch brechen die betreffenden Testfälle nämlich schon beim EntityTransaction.commit()
ab, obwohl eigentlich zu erwarten gewesen wäre, dass sie erst an der Assertion im zweiten Teil des
Tests scheitern, wenn unter der ursprünglich vergebenen ID keine Entität in der Datenbank gefunden
werden kann.
43
eine ID mit @GeneratedValue erzeugt wird. Da sie ja benötigt wird, um die
betreffende Entität in der Datenbank abzulegen, muss sie spätestens beim
EntityTransaction.commit() vorhanden sein. Ob sie jedoch bereits beim
EntityManager.persist()
erzeugt
wird,
hängt
von
der
jeweiligen
Implementierung des Persistenzproviders ab. Während der genaue Zeitpunkt der
ID-Generierung für reale JPA-Projekte meist unerheblich ist, spielt er für die von
meinem Plugin erzeugten Tests doch eine wichtige Rolle. Denn nur, wenn der generierte
Primärschlüssel beim EntityManager.persist() erzeugt wird, kann er danach
im Rahmen des Testfalls ausgelesen werden. Wenn Hibernate die ID erst beim
EntityTransaction.commit() auslesen würde, wäre der Aufbau meiner
Testfälle komplizierter geworden.
Wie man an den vorangegangenen Beispielen sieht, hat der gewählte Persistenzprovider
einen erheblichen Einfluss auf das tatsächliche Verhalten von JPA-Projekten, das
dadurch
leider
nicht
immer
spezifikationskonform
ist.
Insofern
war
der
Blackbox-Ansatz, auf Basis dessen ich meine Tests konzipiert habe, durchaus richtig:
Ich habe keine Annahmen über das vermutliche Verhalten des Persistenzproviders
gemacht, sondern mich nur an der Bedeutung der Annotationen selbst orientiert.
Während die von meinem Plugin erzeugten Tests in den meisten Fällen ihr Ziel
erreichen,
nämlich
eine
verhaltensändernde
Modifikation
der
JPA-Metadaten
aufzudecken, scheitern sie in einigen Fällen aber eben auch an der mangelnden
Spezifikationstreue vom Persistenzprovider Hibernate.
44
6 Schlussbetrachtung
6.1 Ausblick
Aus den obigen Beobachtungen und Überlegungen ergeben sich einige Fragestellungen,
die man in späteren Arbeiten weiter verfolgen könnte. Zuallererst einmal habe ich mich
ja aus Zeitgründen nur auf ein kleines Teilset der möglichen Metadaten beschränkt,
obwohl es durchaus mehr interessante JPA-Elemente gibt, für die man Testschemata
entwerfen könnte. Die Annotation NamedQuery und das Annotationselement
cascade bieten zum Beispiel noch Raum für Untersuchungen in diese Richtung.
Dabei stellt sich auch die Frage, inwieweit andere Persistenzprovider die JPA
spezifikationstreuer implementieren und damit die Ergebnisse der generierten Tests
auch eher dem Erwarteten entsprechen. Ist es eventuell möglich, den Testaufbau
dahingehend
zu
verbessern,
dass
mehr
Unabhängigkeit
vom
jeweiligen
Persistenzprovider erreicht wird? Vielleicht sind Regressionstests allein aber auch
unzureichend, weil man dabei immer auf die Zuarbeit durch den Persistenzprovider
angewiesen ist. Könnte man sie durch andere Tools ergänzen, zum Beispiel durch etwas
Ähnliches wie die in Unterabschnitt 2.3 erwähnten Invarianten?
Diese Abschlussarbeit stellt somit lediglich einen ersten Versuch auf dem Gebiet der
automatischen Testerzeugung für JPA-Metadaten dar, welcher noch beliebig ausgebaut
und verbessert werden kann.
6.2 Zusammenfassung
In
dieser
Abschlussarbeit
wurden
für
ausgewählte
Annotationen
und
Annotationselemente der Java Persistence API auf Basis ihrer jeweiligen Semantik
Testschemata entworfen, mit denen verhaltensändernde Modifikationen an diesen
JPA-Metadaten aufgedeckt werden sollten. Mit diesen Schemata als Grundlage wurde
dann ein prototypisches Tool in Form eines Eclipse-Plugins implementiert, welches
automatische Regressionstests für die ausgewählten Annotationen erzeugt. Die korrekte
45
Funktionsweise dieses Plugins wurde anschließend mit manuellen und automatischen
Testdurchläufen überprüft.
Dabei zeigte sich, dass die von meinem Plugin erzeugten Tests in den meisten Fällen in
der Lage sind, semantische Veränderungen an den untersuchten Annotationen
aufzudecken. Sie sind jedoch auf die genaue Validierung des JPA-Projekts durch den
Persistenzprovider angewiesen. Diese findet allerdings - zumindest bei Hibernate - nicht
immer statt, weshalb die generierten Tests in manchen Konstellationen versagen und
eine vorhandene verhaltensändernde Modifikation nicht erkennen können. Da es sich
bei der JPA um eine reine Schnittstellendefinition handelt, ist man von ihrer jeweiligen
Implementierung durch einen Persistenzprovider abhängig. Wenn man das Verhalten
eines JPA-Projektes losgelöst vom verwendeten Persistenzframework betrachten will,
hat man die Rechnung leider ohne den Wirt gemacht.
46
Anhang
Listing 1: Generierter Code für @...ToMany-Test
47
Literaturverzeichnis
1. Baresi, L., Miraz, M.: TestFul: automatic unit-test generation for Java classes.
In: Proceedings of the 32nd ACM/IEEE International Conference on Software
Engineering, May 01-08, 2010, Cape Town, South Africa
2. Beck, K., Gamma, E., Saff, D., Clark, M.: JUnit. http://junit.org
3. Darwin, I.: AnnaBot: a static verifier for java annotation usage. In: Advances in
Software Engineering, 2010. pp.1-13
4. DeMichiel, L.: JSR 338: Java Persistence API, Version 2.1, JSR Specification.
https://jcp.org/en/jsr/detail?id=338
5. Eclipse Foundation: Acceleo. https://eclipse.org/acceleo
6. Eclipse
Foundation:
Acceleo
Documentation.
http://help.eclipse.org/luna/index.jsp?topic=%2Forg.eclipse.acceleo.doc
%2Fpages%2Freference%2Flanguage.html
7. Fraser, G., Arcuri, A.: EvoSuite: automatic test suite generation for
object-oriented software. In: Proceedings of the 19th ACM SIGSOFT
symposium and the 13th European conference on Foundations of software
engineering, September 05-09, 2011, Szeged, Hungary
8. Mayer, P., Steimann, F., Ulke, B.: Dr. Deepfix or: How I learned to stop chasing
error messages and love constraints . Dezember 2014, unveröffentlichtes
Manuskript
9. Object Management Group: MOF Model to Text Transformation Language
(MOFM2T), 1.0. http://www.omg.org/spec/MOFM2T/1.0
10. Pacheco, C., Ernst, M.D.: Randoop: feedback-directed random testing for Java.
48
In: Companion to the 22nd ACM SIGPLAN conference on Object-oriented
programming systems and applications companion, October 21-25, 2007,
Montreal, Quebec, Canada
11. Red Hat Inc: Arquillian. http://arquillian.org
12. Red
Hat
Inc:
Hibernate
Community
Documentation.
https://docs.jboss.org/hibernate/annotations/3.5/reference/en/html_single
13. Red Hat Inc: Hibernate ORM. http://hibernate.org/orm
14. Song, M., Tilevich, E.: Metadata invariants: checking and inferring metadata
coding conventions. In: Proceedings of the 2012 International Conference on
Software Engineering, June 02-09, 2012, Zurich, Switzerland
15. Zhang, S.: Palus: a hybrid automated test generation tool for java. In:
Proceedings of the 33rd International Conference on Software Engineering, May
21-28, 2011, Waikiki, Honolulu, HI, USA
49
Herunterladen