Im letzten Blog der Serie über JPA-Implementierungsmuster habe ich über die drei Standardmethoden zur Abbildung von Vererbungshierarchien mit JPA gesprochen. Und eine nicht standardmäßige, aber sehr nützliche Methode vorgestellt. Diese Woche werde ich verschiedene Ansätze zum Testen von JPA-Code besprechen.
Was soll getestet werden?
Die erste Frage, die sich stellt, ist: Welchen Code wollen wir testen? Wenn wir über JPA sprechen, geht es um zwei Arten von Objekten: Domänenobjekte und Datenzugriffsobjekte (DAOs). Theoretisch sind Ihre Domänenobjekte nicht an JPA gebunden (es sind POJOs, richtig?), so dass Sie deren Funktionalität ohne einen JPA-Provider testen können. Darüber gibt es hier nichts Interessantes zu diskutieren. Aber in der Praxis werden Ihre Domänenobjekte zumindest mit JPA-Annotationen versehen sein und vielleicht auch etwas Code enthalten, um bidirektionale Assoziationen(lazily), Primärschlüssel oder serialisierte Objekte zu verwalten. Jetzt wird es noch interessanter...
(Auch wenn dieser JPA-spezifische Code gegen die POJO-Eigenschaft der Domänenobjekte verstößt, muss er vorhanden sein, damit die Domänenobjekte immer auf die gleiche Weise funktionieren. Ob innerhalb oder außerhalb eines JPA-Containers. Die Verwaltung von bidirektionalen Assoziationen und die Verwendung von UUIDs als Primärschlüssel sind schöne Beispiele dafür. Auf jeden Fall ist dies ein Code, den Sie unbedingt testen müssen.)
Natürlich müssen wir auch die DAOs testen, nicht wahr? Hier taucht eine interessante Frage auf:
Gegen was soll getestet werden?
Da wir nun wissen, was wir testen wollen, können wir entscheiden, wogegen wir testen wollen. Da wir Datenbankcode testen, soll unsere Testvorrichtung eine Datenbank enthalten. Diese Datenbank kann eine eingebettete In-Memory-Datenbank wie HSQLDB (im reinen Speichermodus) oder eine "echte" Datenbank wie MySQL oder Oracle sein. Die Verwendung einer eingebetteten Datenbank hat den großen Vorteil, dass sie einfach einzurichten ist. Es ist nicht erforderlich, dass jeder, der die Tests ausführt, über eine laufende MySQL- oder Oracle-Instanz verfügt. Wenn Ihr Produktionscode jedoch gegen eine andere Datenbank läuft, werden Sie auf diese Weise möglicherweise nicht alle Datenbankprobleme erkennen. Daher ist auch ein Integrationstest gegen eine echte Datenbank erforderlich, aber dazu später mehr. Für die meisten Tests benötigen wir mehr als nur die Datenbank. Wir müssen sie vor einem Test korrekt einrichten und nach dem Test müssen wir sie für den nächsten Test in einem brauchbaren Zustand belassen. Das Einrichten des Schemas und das Befüllen der Datenbank mit den richtigen Daten vor der Ausführung des Tests sind nicht so schwierig (dies sei dem Leser als Übung überlassen ;-) ), aber die Datenbank nach dem Test wieder in einen brauchbaren Zustand zu versetzen ist ein schwierigeres Problem. Ich habe eine Reihe von Ansätzen für dieses Problem gefunden:
- Das Spring Framework enthält ein Test-Framework, das Transaktionen verwendet, um den Zustand Ihrer Testvorrichtung zu verwalten. Wenn Sie Ihren Test so annotieren, dass er @Transaktionalist, wird der SpringJUnit4ClassRunner vor jedem Teststart eine Transaktion starten und diese Transaktion am Ende des Tests wieder zurücknehmen, um zu einem bekannten Zustand zurückzukehren. Wenn Sie noch JUnit 3.8 verwenden, können Sie die AbstractTransactionalSpringContextTests Basisklasse erweitern, um den gleichen Effekt zu erzielen. Das mag nett erscheinen, aber in der Praxis habe ich diese Methode aus mehreren Gründen als unbefriedigend empfunden:
- Standardmäßig wird der JPA-Kontext erst geleert, wenn die Transaktion bestätigt oder eine Abfrage ausgeführt wird. Wenn Ihr Test also keine Abfrage enthält, werden Änderungen nicht an die Datenbank weitergegeben, was Probleme mit ungültigen Mappings und dergleichen verbergen kann. Sie könnten versuchen, explizit die Funktion EntityManager.flush vor dem Ende des Tests aufzurufen, aber dann entsprechen die Tests nicht mehr dem realen Szenario.
- Wenn Sie eine Entität speichern und sie dann in derselben Sitzung wieder abrufen, treten auch nicht die unangenehmen Probleme des "Lazy Loading" auf. Sie greifen wahrscheinlich nicht einmal auf die Datenbank zu, da der JPA-Provider einen Verweis auf das Objekt zurückgibt, das Sie gerade gespeichert haben!
- Schließlich möchten Sie in einem Test vielleicht zunächst einige Daten in der Datenbank speichern, dann die Tests ausführen und schließlich überprüfen, ob die richtigen Daten in die Datenbank geschrieben wurden. Um dies richtig zu testen, benötigen Sie drei separate Transaktionen, ohne dass die ersten beiden Transaktionen zurückgenommen werden.
- Wenn Sie eine eingebettete In-Memory-Datenbank verwenden, ist diese Datenbank sauber, wenn Sie den ersten Test ausführen, und Sie müssen sich nicht darum kümmern, sie in einem guten Zustand zu belassen, nachdem alle Tests ausgeführt worden sind. Das bedeutet, dass Sie keine Transaktionen zurücknehmen müssen und mehrere Transaktionen innerhalb eines Tests durchführen können. Allerdings müssen Sie möglicherweise zwischen den einzelnen Tests etwas Besonderes tun. Wenn Sie zum Beispiel das Spring TestContext-Framework verwenden, können Sie die @DirtiesContext Annotation verwenden, um die In-Memory-Datenbank zwischen den Tests neu zu initialisieren.
- Wenn Sie keine In-Memory-Datenbank verwenden können oder eine Neuinitialisierung nach jedem Test zu teuer ist, können Sie versuchen, alle Tabellen nach jedem Test (oder vor jedem Test) zu löschen. Mit DbUnit können Sie zum Beispiel alle Daten aus Ihren Testtabellen löschen oder alle Testtabellen abschneiden. Fremdschlüssel-Beschränkungen können jedoch im Weg stehen, so dass Sie die referentielle Integrität vorübergehend deaktivieren sollten, bevor Sie diese Operationen durchführen.
In welchem Umfang soll getestet werden?
Als nächstes müssen Sie sich für den Umfang der Tests entscheiden. Werden Sie kleine Unit-Tests, größere Komponententests oder umfassende Integrationstests schreiben? Aufgrund der Art und Weise, wie JPA funktioniert (die Undichtigkeit der Abstraktion wenn Sie so wollen), treten manche Probleme nur in einem größeren Kontext auf. Während das isolierte Testen der persist-Methode für DAO nützlich ist, wenn Sie wissen wollen, ob die Grundlagen korrekt sind, müssen Sie in einem größeren Rahmen testen, um die Fehler im Zusammenhang mit Lazy Loading oder der Transaktionsverarbeitung zu finden. Um Ihr System wirklich zu testen, müssen Sie kleine Unit-Tests mit größeren Komponententests kombinieren, bei denen Sie Ihre Service-Fassaden mit den zu testenden DAOs verkabeln. Für beide Tests können Sie eine In-Memory-Datenbank verwenden. Und um Ihre Testabdeckung zu vervollständigen, benötigen Sie einen Integrationstest mit der Datenbank, die Sie in der Produktion mit einem Tool wie
Was soll ich behaupten?
Ein letzter Punkt, den Sie angehen müssen, ist die Frage, was in den Tests überprüft werden soll. Es könnte sein, dass Ihre Domänenobjekte einem bestehenden Schema zugeordnet sind. In diesem Fall möchten Sie sicherstellen, dass die Zuordnung korrekt ist. In diesem Fall möchten Sie den rohen JDBC-Zugriff auf die zugrunde liegende Datenbank verwenden, um sicherzustellen, dass die richtigen Änderungen an den richtigen Tabellen vorgenommen wurden. Wenn das Schema jedoch automatisch aus der JPA-Zuordnung generiert wird, ist Ihnen das tatsächliche Schema wahrscheinlich egal. Sie wollen sicherstellen, dass persistierte Objekte in einer neuen Sitzung korrekt abgerufen werden können. Ein direkter Zugriff auf das zugrunde liegende Schema mit JDBC ist nicht notwendig und würde einen solchen Testcode nur spröde machen. Ich bin mir ziemlich sicher, dass ich in diesem Blog nicht alle Testszenarien behandelt habe, denn das Testen von Datenbankcode ist ein sehr kompliziertes Gebiet. Ich würde mich freuen zu hören, wie Sie Ihren Datenbankcode testen, egal ob er JPA oder einen anderen Persistenzmechanismus verwendet.
Verfasst von
Vincent Partington
Unsere Ideen
Weitere Blogs
Contact



