Projekte kommen heutzutage in der Regel nicht mehr ohne den Einsatz von Fremdbibliotheken aus. Ein Neuerfinden/-schreiben bereits vorhandener Funktionalitäten ist meistens unwirtschaftlich und bremst die Produktivität. Schließlich sollen fachliche Probleme gelöst und nicht primär technischer Code geschrieben werden.
Dasselbe Argument gilt natürlich nicht nur für den Produktiv-, sondern auch für Testcode. Auch hier gibt es für die verschiedensten Anforderungen bereits fertige Bibliotheken, die nur noch eingebunden werden müssen.
Mit wachsender Anzahl wird es jedoch zunehmend schwerer den Überblick zu behalten beziehungsweise zu wissen, ob es für das aktuelle Problem bereits eine fertige Bibliothek gibt. Auch eine Suche im Internet wird aufgrund der großen Anzahl an Treffern immer schwieriger. Dieser Artikel stellt darum sechs Java-Bibliotheken (s. Tabelle 1) vor, die Entwicklern beim Schreiben von Tests unter die Arme greifen. Diese haben sich in der Praxis bewährt und wurden vom Autor bereits in mehreren Projekten erfolgreich eingesetzt.
Bibliothek | Link |
---|---|
Awaitility | https://github.com/awaitility/awaitility |
EqualsVerifier | http://jqno.nl/equalsverifier |
Java Faker | http://dius.github.io/java-faker |
Log Collectors | https://github.com/haasted/TestLogCollectors |
Make It Easy | https://github.com/npryce/make-it-easy |
System Rules | https://stefanbirkner.github.io/system-rules/index.html |
EqualsVerifier
An vielen Stellen werden in Java-Programmen Objekte miteinander verglichen.
Standardmäßig prüft Java dabei für Klassen, ob es sich bei den zu vergleichenden
Objekten um dieselbe Instanz handelt. Soll dieses Verhalten geändert werden,
muss die Methode equals
aus java.lang.Object
überschrieben werden. Die so
überschriebene Methode muss anschliessend jedoch immer fünf Eigenschaften
aufweisen:
- Die Implementierung muss reflexiv sein. Das bedeutet, dass die Methode
true
zurückgeben muss, wenn überprüft wird, ob eine Instanz zu sich selbst identisch ist. - Die zweite Eigenschaft ist die der Symmetrie. Hierbei ist sicherzustellen,
dass wenn
x
identisch zuy
ist, auchy
identisch zux
ist. - Zudem muss die Methode auch transitiv sein. Es muss also gelten, dass wenn
x
identisch zuy
undy
identisch zuz
ist, dass auchx
undz
identisch sein müssen. - Außerdem muss das Ergebnis konsistent sein, also mehrere Aufrufe müssen immer dasselbe Ergebnis zurückgeben, solange nicht eines der beiden Objekte in der Zwischenzeit modifiziert wurde.
- Die letzte Eigenschaft fordert, dass ein Vergleich mit
null
immerfalse
zurückzugeben hat.
Neben diesen generellen Eigenschaften besteht auch noch eine Kopplung an die
Methode hashCode
. Dies führt dazu, dass in der Regel beide Methoden
überschrieben werden müssen. Auch hashCode
fordert, dass mehrere Aufrufe der
Methode immer dasselbe Ergebnis zurückgeben, solange die Instanz in der
Zwischenzeit nicht geändert wurde. Zudem muss das Ergebnis von Aufrufen auf
gleichen Objekten identisch sein. Für unterschiedliche Objekte gibt es keine
Vorgabe, es ist jedoch sinnvoll, hier dafür zu sorgen, dass unterschiedliche
Ergebnisse entstehen.
Besonders spannend wird diese Thematik vor allem, wenn Vererbung im Einsatz ist. Gerade die Eigenschaft der Symmetrie wird hierbei schnell verletzt.
Aus diesem Grund bietet es sich an, für Klassen, bei denen equals
und
hashCode
überschrieben wird, Tests zu schreiben, um deren Richtigkeit zu
überprüfen. Um hierbei nicht jedes Mal wieder das Rad neu zu erfinden, kann die
Bibliothek EqualsVerifier verwendet werden. Mit dieser reduziert sich das Testen
aller Eigenschaften auf einen einzelnen Test (s. Listing 1).
EqualsVerifier setzt für die Überprüfung Reflection ein und trifft einige
Annahmen, um auch Randfälle überprüfen zu können. So wird beispielsweise
gefordert, dass die Klasse oder die beiden Methoden equals
und hashCode
alle
final
sind, dass beide Methoden nicht von einem änderbaren Feld abhängen oder
dass alle Felder einbezogen werden.
Erfüllt der eigene Code nicht alle Annahmen, ist es möglich, EqualsVerifier mitzuteilen, dass diese ignoriert werden sollen. Hierfür sollte allerdings immer ein guter Grund bestehen.
Awaitility
Das Testen synchroner Methodenaufrufe ist relativ einfach. Der initiale Zustand wird aufgesetzt, die zu testende Methode wird aufgerufen und anschließend überprüft man das Ergebnis des Methodenaufrufes oder ob sich an einer anderen Stelle etwas, durch einen Seiteneffekt, geändert hat.
Bei asynchroner Verarbeitung ist dies bereits schwieriger. Der Aufruf auf der zu testenden Methode gibt dem Test sofort die Kontrolle zurück. Natürlich kann dieser jetzt direkt versuchen, Dinge zu überprüfen. Durch die nun auftretende parallele Verarbeitung kann es aber passieren, dass der Test schneller ist und deswegen Dinge überprüft, die erst später passieren werden.
Genau hier setzt die Bibliothek Awaitility an. Diese erlaubt es mit einfachen Mitteln, nach dem Aufruf einer asynchronen Methode im Test zu warten, bis eine bestimmte Bedingung erreicht wurde. Da dies die Gefahr birgt, dass der Test nun ewig wartet, wenn die Bedingung nie erfüllt wird, wird zudem standardmäßig nur maximal zehn Sekunden gewartet. Tritt die Bedingung nicht innerhalb dieser Zeit ein, wird eine Exception geworfen. Natürlich lässt sich dieser Timeout auch für jeden Aufruf einzeln konfigurieren. Listing 2 zeigt den Einsatz von Awaitility inklusive Konfiguration des Timeouts.
Um die Wartebedingungen zu formulieren, wird standardmäßig Hamcrest verwendet. Es ist jedoch auch möglich, diese mit AssertJ zu formulieren.
Zudem ist es möglich, Einfluss auf den Thread- und Poll-Mechanismus zu nehmen. Wird nichts angegeben, nutzt Awaitility ein fixes Intervall, um zu prüfen, ob die formulierte Erwartung bereits eingetroffen ist. Neben dieser Strategie bringt Awaitility bereits ein Fibonacci- und ein iteratives Intervall mit.
System Rules
Wird java.lang.System
genutzt, erleichtert einem die nützliche Bibliothek
System Rules das Leben. Die drei Regeln SystemErrRule
, SystemOutRule
und
TextFromStandardInputStream
helfen vor allem dabei, Tests für
Kommandozeilen-Anwendungen zu schreiben. Die ersten beiden Regeln ermöglichen
dabei das Überprüfen, ob erwartete Texte geschrieben wurden, die letzte hilft
dabei, Eingaben in die Anwendung hineinzugeben. Listing 3 zeigt, wie diese drei
genutzt werden können.
Wird eine Anwendung mit grafischer Oberfläche entwickelt, sollte in der Regel
nicht mittels System.out
oder System.err
geloggt werden, sondern über ein
Logging-Framework. Um sicherzustellen, dass dies der Fall ist, lassen sich die
Regeln DisallowWriteToSystemOut
und DisallowWriteToSystemErr
verwenden (s.
Listing 4).
Vor allem bei Kommandozeilen-Anwendungen relevant ist der Einsatz von
System.exit
. Mit ExpectedSystemExit
kann sichergestellt werden, dass die
Anwendung mit dem passenden Exit-Code beendet wurde (s. Listing 5).
Zudem bietet System Rules noch Unterstützung für System- und Umgebungsvariablen.
ClearSystemProperties
entfernt die spezifizierte Systemvariable vor dem Test
und ProvideSystemProperty
setzt sie auf einen vorgegebenen Wert. Beide sorgen
dafür, dass nach dem Test der vorher existierende Stand wiederhergestellt wird.
RestoreSystemProperties
wird verwendet, um sämtliche während des Tests
erfolgten Änderungen anschließend zu verwerfen. EnvironmentVariables
ist das
Pendant zu ProvideSystemProperty
für Umgebungsvariablen. Beispiele für diese
vier Regeln sind in Listing 6 zu sehen.
Als letztes gibt es noch die Regel ProvideSecurityManager
, mit der man einen
eigenen SecurityManager
für einen einzelnen Test setzen kann.
Zum aktuellen Zeitpunkt wird von System Rules nur JUnit 4 unterstützt. Es wird jedoch bereits seit einiger Zeit am Support für JUnit 5 gearbeitet.
Java Faker
Häufig müssen in Tests Daten aufgesetzt werden. Um sich nicht immer kreative Werte selbst ausdenken zu müssen, kann Java Faker verwendet werden. Java Faker ist ein Port der beliebten Ruby Faker-Bibliothek, welche wiederum selbst ein Port der Perl-Bibliothek Faker ist.
Java Faker unterstützt mittlerweile über 25 Arten von Daten in über 40 Sprachen.
Listing 7 zeigt, wie die Bibliothek verwendet wird. Um die Tests im Fehlerfall
mit den gleichen generierten Daten ausführen zu können, sollte man die
Initialisierung mit einer Instanz von java.util.Random
nutzen. Diese
Random-Instanz kann bei jedem Start mit einem zufällig gewählten Integer
erzeugt werden. Allerdings sollte man sich den Integer
ausgeben lassen, um
diesen beim Nachstellen auf einen fixen Wert zu setzen.
Make It Easy
Neben passenden Daten müssen die Objekte in den Tests natürlich auch in einen passenden Zustand versetzt werden. Als Muster hat sich hier das Builder-Pattern etabliert. Das manuelle Schreiben von komplexeren Buildern kann allerdings schnell aufwendig werden und in viel Code ausarten. Make It Easy wurde genau für diesen Fall gebaut. Listing 8 zeigt, wie man mit wenigen Zeilen Code einen Builder für einen Pkw erzeugen kann.
Builder lassen sich natürlich auch auf andere Arten, zum Beispiel mit der @Builder-Annotation von Project Lombok, erzeugen. Allerdings ist es mit Make It Easy möglich, Builder zu generieren, ohne dass die Klassen, die man erzeugen möchte, modifiziert werden müssen.
Log Collectors
Die nächste Bibliothek vereinfacht das Überprüfen von Log-Nachrichten. Üblicherweise müssen diese zwar nicht getestet werden. Ab und zu gibt es jedoch Anforderungen an das Logging, die über Tests abgedeckt werden sollten. Hierzu kann Log Collectors eingesetzt werden. Dabei werden JUnit in Version 4 und 5 sowie TestNG unterstützt und auch die Unterstützung für verschiedene Logging-Implementierungen ist mit Log4J2, Logback, java.util.logging und slf4j nahezu vollständig. Listing 9 zeigt den Einsatz in JUnit 4 für slf4j.
Neben der Methode getLogs
, die eine Liste aller geloggten Nachrichtentexte
zurück liefert, gibt es mit getRawLogs
noch die Möglichkeit, an die kompletten
Log-Nachrichten zu gelangen. Hiermit lassen sich dann auch noch weitere Dinge,
wie beispielsweise die Anzahl der vorhandenen Nachrichten für ein bestimmtes
Log-Level, überprüfen.