Unit-Tests sind einer der Eckpfeiler der modernen Softwareentwicklung. Bei der Arbeit an mehreren Projekten mit Spring und Hibernate bin ich zu dem Schluss gekommen, dass es (in dieser Umgebung?) eigentlich drei Arten von Unit-Tests gibt:
Untereinheit einer anderen Klasse betrachtet werden, die keinen separaten Test wert ist. Meiner Erfahrung nach kann ein zufriedenstellendes Maß an Isolation nur durch Mocking erreicht werden. Der Grund dafür ist, dass nur durch Mocking sichergestellt werden kann, dass bei einer Änderung der Funktionalität einer Klasse diese Änderung nicht kaskadenartig in die Tests aller Klassen einfließt, die diese Funktionalität verwenden. Genau darum geht es bei der Isolation. Wir verwenden RMock , obwohl man munkelt, dass bei einem Java5-Projekt die neueste Version von EasyMock besser wäre. Ein schwerwiegender Nachteil dieses Ansatzes ist meiner Meinung nach, dass er TDD nicht wirklich inspiriert. Eine der Grundlagen von TDD besteht darin, dass das Design der Lösung aus dem Test folgen sollte und der Test direkt aus einer Anforderung folgen sollte. Die meisten Anforderungen lassen sich nicht einfach als "füge diese Funktionalität zu dieser Klasse hinzu" interpretieren. Um die Klassenebene zu erreichen, ist ein gewisses Maß an (Vorab-)Design erforderlich. Anstatt also einen Test zu schreiben, der die Anforderung kapselt, wird die Anforderung in Funktionalität zerlegt, die Klassen zugewiesen wird (das ist der Entwurf im Vorfeld) und diese Funktionalität wird getestet.
Testen der DAO-Schicht
Wenn Sie Hibernate oder ein anderes Framework für Ihre DAO-Schicht verwenden, landet eine Menge Geschäftslogik in Units oder Artefakten, die mit einfachen Unit-Tests nicht testbar sind. Bei Hibernate wird die Geschäftslogik in die Mapping-Dateien und die Abfragen injiziert.
Hibernate-Mappings ermöglichen es der Anwendung im Grunde, CRUD-Operationen auf Entitäten durchzuführen. Ein einfacher Test würde damit beginnen, eine Entität einzufügen, zu prüfen, ob sie abgerufen werden kann, sie zu aktualisieren, erneut zu prüfen und sie dann zu löschen. Es ist nicht schwer, sich ein Test-Framework für diese Aufgabe vorzustellen. In den Konfigurationsdateien von Hibernate werden jedoch auch komplexe Eigenschaften wie Kaskadierung und Inversität definiert. Die korrekte Konfiguration dieser Eigenschaften ist für Ihre Anwendung ebenso wichtig. Das Testen dieser Eigenschaften ist etwas komplizierter und beinhaltet die Navigation durch Assoziationen und Sammlungen.
Das Testen von Abfragen ist etwas komplizierter. Auf der Grundlage einer HQL-, SQL- oder Criteria-Abfrage generiert Hibernate SQL, das gegen ein bestimmtes RDBMS ausgeführt werden kann. Ein Ansatz zum Testen der Abfrage wäre die Überprüfung, ob die generierte Abfrage korrekt ist. Dazu gehört das Parsen der erzeugten SQL und die Überprüfung bestimmter Merkmale wie das Vorhandensein von Tabellen in der FROM-Klausel, die richtigen Einschränkungen in der WHERE-Klausel und die Spalten in der SELECT-Klausel. Das scheint ziemlich mühsam zu sein. Ein anderer Ansatz wäre, das SQL tatsächlich gegen eine Datenbank auszuführen und die Ergebnisse zu überprüfen. Mit einer speicherinternen Datenbank wie HSQLDB ist dies eine recht einfache Aufgabe. Ein wichtiger Nebeneffekt dabei ist, dass Hibernate für verschiedene RDBMS-Typen unterschiedliche SQL generiert. Wenn das Test-Setup einen anderen RDBMS-Typ verwendet, werden die Abfragen, die in der Einsatzsituation verwendet werden, während der Unittest-Phase eigentlich nie getestet.
Daten sind ein weiteres Problem, wenn Tests gegen ein tatsächliches RDBMS laufen. Um die Logik einer Abfrage überprüfen zu können, sollte sie gegen eine Reihe verschiedener Datensätze ausgeführt werden. Normalerweise reagiert die Abfrage sowohl auf Parameter als auch auf die tatsächlichen Daten im System. Beide Empfindlichkeiten sollten mit verschiedenen Variationen getestet werden. Eine Möglichkeit, Daten in das System zu bekommen, um Tests zu ermöglichen, ist die Verwendung von DBUnit. Dieses Framework liest XML-Dateien und erstellt Einfügeanweisungen, die gegen das RDBMS ausgeführt werden können. Das Problem bei diesem Ansatz ist, dass die Testerwartungen und -ergebnisse für verschiedene Tests dazu neigen, miteinander korreliert zu werden, da die Daten für den gesamten Testfall aus derselben XML-Datei gelesen werden. Diese Datei neigt dazu, sehr groß und schwer verständlich zu werden. Die verschiedenen Varianten, die getestet werden, sind kaum dokumentiert und bei einer schnellen Durchsicht der Datei nicht ersichtlich.
Eine Lösung wäre, für jeden Test eine eigene Datei zu verwenden. In diesem Fall werden die Tests nicht korreliert und ein Kommentar am Anfang der Datei könnte die getestete Variante beschreiben. Dies würde wahrscheinlich zu einer großen Anzahl von sehr ähnlichen Dateien im Projekt führen. Aufgrund von Fremdschlüssel-Beschränkungen wirkt sich das Einfügen eines einzelnen Datensatzes in die Datenbank oft auf eine Vielzahl von Bezugstabellen aus, die Referenzdaten sind. Dies könnte durch die Verwendung einer hierarchischen Struktur gelöst werden, bei der zunächst Referenzdaten geladen und mit einer Reihe kleinerer und spezifischerer Datensätze verwendet werden.
Eine andere Lösung wäre, die für den Test erforderlichen Objekte im Code zu erstellen und sie mit Hibernate zu persistieren. Die daraus resultierenden Tests wären leichter zu refaktorisieren und würden unabhängiger sein. Es könnte zu einer Menge Code-Duplizierung führen, aber das könnte durch die Verwendung des ObjectMother Patterns gelöst werden. Ein weiterer Vorteil dieses Ansatzes wäre, dass die Mapping-Dateien indirekt getestet werden. Das könnte sogar ein Nachteil sein, denn wenn ein Mapping beschädigt ist, könnten völlig unzusammenhängende Tests fehlschlagen.
Wie immer ist die Realität nicht einfach und die beste Lösung wird eine Mischung aus beidem sein. Ich habe tatsächlich noch nie versucht, beide Strategien (DBunit und ObjectMother) im selben Projekt zu kombinieren. Ich bin sehr neugierig, was die Fallstricke sein werden. Ein Problem, das wahrscheinlich bei jeder Strategie bestehen bleibt, ist, dass die Testabdeckung nicht bestimmt werden kann (oder doch?).
Testen der Komponentenintegration
Bei der Verwendung von Spring werden die Einheiten in einem Anwendungskontext zu 'Komponenten' oder Beans der obersten Ebene zusammengeklebt. Der Anwendungskontext wird in der Unit-Testphase nie wirklich verwendet, da sein einziger Zweck darin besteht, Einheiten zusammenzukleben, die in dieser Phase isoliert getestet werden. Der Anwendungskontext ist eine der wichtigsten Dateien in Ihrer Anwendung. Das Starten einer Anwendung mit einem Tippfehler im Anwendungskontext wird kläglich scheitern, wenn auch zum Glück frühzeitig.
Einige der Eigenschaften und Strukturen, die im Anwendungskontext definiert sind, können weitreichende Auswirkungen auf Ihre Anwendung haben. Normalerweise werden hier Transaktionsgrenzen definiert und auch andere übergreifende Belange. Den Anwendungskontext nicht zu testen, sollte meiner Meinung nach als schwerwiegender Mangel beim automatischen Testen einer Anwendung angesehen werden. Einige der Komponenten, die im Einsatz verwendet (und somit im Anwendungskontext konfiguriert) werden, können in einer Testsituation nicht verwendet werden. Ein Beispiel hierfür sind Schnittstellen zu Backend-Systemen. Um dies zu erleichtern, sollten diese Komponenten in einem separaten Anwendungskontext definiert werden, der beim Testen weggelassen oder ersetzt werden kann.
Andererseits sind diese Art von Tests den Integrationstests sehr ähnlich. Ich denke, der Unterschied sollte darin bestehen, dass die "Komponentenintegrationstests" kaum die Ergebnisse der Anwendung überprüfen. Wenn eine bestimmte Servicemethode ausgeführt werden kann, ohne auf eine NullPointerException zu stoßen, und sie etwas ausgibt, das auf den ersten Blick sinnvoll erscheint, ist der Test erfolgreich. Weitere Tests sollten als Teil der Funktions- und Integrationstests betrachtet werden.
Fazit
Meiner Meinung nach gibt es mehrere Schritte, die in der Unit-Testphase nacheinander ausgeführt werden sollten. Ein Fehler in einem frühen Schritt sollte den Erstellungsprozess beenden, ohne die anderen Schritte auszuführen, da deren fehlerhafte Ausgabe auf dem zuvor gefundenen Fehler beruhen könnte. Einer meiner Kollegen schlug TestNG vor, um Testgruppen zu erstellen. Die oben beschriebenen Testtypen können als Ausgangspunkt für diese Schritte verwendet werden. Vielleicht gibt es noch andere Arten von Tests, die eine eigene Gruppe verdienen. Die oben beschriebenen Methoden zum Schreiben der Tests scheinen vielversprechend, sind aber noch keine bewährte Technologie. Halten Sie die Gruppierung von Unittests für sinnvoll? Kennen Sie die oben beschriebenen Gruppen und Richtlinien?
- Grundlegende Unit-Tests
- Dao Layer Unit Tests
- Testen der Komponentenintegration
Verfasst von
Maarten Winkels
Contact
Let’s discuss how we can support your journey.



