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