Nach sechs Jahren und bisher 37 Kolumnen ist meine Standardantwort, wenn ich auf das Schreiben von Artikeln angesprochen werde, dass ich das Schreiben selbst nicht so kompliziert finde, aber die Themenfindung von Mal zu Mal schwerer wird.
So ist es mir auch dieses Mal wieder passiert, dass ich an der Themenfindung verzweifelt bin und die Frist zur Abgabe gekommen ist. Einerseits möchte ich dem Namen der Kolumne „Der Praktiker“ gerecht werden, andererseits ist die Praxis nicht immer so spannend, wie wir uns das wünschen. Viele Themen meines Alltags sind zu spezifisch oder füllen thematisch keinen ganzen Artikel. Gleichzeitig sollte es mit Java oder der JVM zu tun haben und idealerweise noch nah am Code bleiben.
Aus diesem Grund habe ich diese Kolumne unter dem Motto „bitte eine gemischte Tüte Süßes für 50 Pfennig“ begonnen und habe erst am Ende meines Schreibprozesses festgestellt, dass alle behandelten Themen einen Bezug zum Testen von und mit Java haben und es somit wenigstens einen übergreifenden Slogan geben kann.
Lange Rede, kurzer Sinn. Auch wenn es dieses Mal keinen durchgehenden roten Faden gibt, wünsche ich viel Spaß beim Lesen und hoffe, dass zumindest ein hier behandeltes Thema von Interesse ist.
Testdaten mit Instancio generieren
In Tests, egal auf welcher Ebene und auch unabhängig von der konkreten Implementierung, nimmt schnell die Initialisierung von Testdaten einen großen Platz ein. Häufig interessieren uns dabei für den konkreten Test nur einige wenige Eigenschaften. Die restlichen sind irrelevant, obwohl diese verpflichtend sind. Eine Lösung, um das Set-up zu reduzieren, besteht darin, das Object-Mother-Muster einzusetzen. Eine solche Klasse hat die Aufgabe, konkrete, valide Objekte zu erzeugen. Um Überraschungen zu vermeiden, empfiehlt es sich, hierbei nur verpflichtende Werte beim Erzeugen zu setzen. Somit entsteht ein minimal valides Objekt (s. Listing 1).
Dieser Ansatz bringt jedoch ein Problem mit sich. Ein so erzeugtes, valides Objekt enthält Werte für alle relevanten Attribute. Somit ist in einem Test, der sich auf diese hartcodierten Werte verlässt, nicht mehr sichtbar, welche Attribute nun konkret relevant sind. Eine mögliche Lösung für dieses Problem besteht darin, unsere ObjectMother mit dem FluentInterface-Muster zu kombinieren. Die Tests können dann wie in Listing 2 aussehen.
Obwohl wir nun in unseren Tests sehen können, welche Attribute relevant sind, schleicht es sich mit der Zeit ein, dass wir uns doch auf einen derhartcodierten Werte verlassen und nicht bedacht haben, dass dieses Attribut für den Test doch eigentlich relevant ist. So entsteht über die Zeit eine hohe Abhängigkeit zu den konkreten Werten, und unsere Tests sind leider doch nicht so leicht verständlich wie gewünscht.
Um nun auch noch dieses Problem zu lösen, bietet es sich an, die verpflichtenden Attribute bei der Erzeugung, innerhalb unserer ObjectMother, mit zufälligen Werten zu füllen. Listing 3 zeigt, wie das, mit Unterstützung von Apache Commons Lang, aussehen kann.
Anstelle von Apache Commons Lang hätten wir auch das bereits von mir in „Java-Bibliotheken für den Einsatz in Tests“ vorgestellte Java Faker nutzen können. Doch auch hiermit bleibt uns eine Menge manuelle Arbeit. Wir müssen die ObjectMother-Klassen schreiben und je nach Anzahl von Attributen und Modellklassen kann das eine ganze Menge sein.
Genau hier setzt Instancio an. Instancio ermöglicht es uns, mit wenigen Zeilen Code ein komplettes Objekt mit Werten zu befüllen. Wir können dabei, siehe Listing 4, pro Attribut festlegen, nach welchen Regeln der Wert generiert wird.
Möchten wir zusätzlich auf die Strings zur Angabe von Feldnamen verzichten, können wir mittels Annotation Processor ein Metamodell generieren und anschließend, siehe Listing 5, im Code nutzen. Zudem bringt Instancio noch direkte Unterstützung für JUnit 5 mit. Diese bietet uns noch zwei weitere Vorteile.
Zum Ersten enthalten nun die Ausgaben bei einem fehlschlagenden Test die Nummer
des Seeds, der für die Generierung der Zufallsdaten verwendet wurde. Wir haben
nun die Möglichkeit, diese Nummer mittels @Seed
-Annotation am Test, siehe
Listing 6, oder bei Erzeugung der Instancio
-Instanz anzugeben. Anschließend
erhalten wir bei jedem Lauf exakt dieselben zufällig generierten Werte und
können den Testlauf somit reproduzieren.
Zum Zweiten enthält die JUnit 5 Unterstützung von Instancio mit der
@InstancioSource
-Annotation eine fertige Lösung für parametrisierte Tests
(s. Listing 7).
Eintauchen mit Deep Dive
Auch wenn JUnit 5 bereits Methoden zur Überprüfung, Assertion, von Werten mitbringt, ist es – mittlerweile – üblich, hierzu eine eigene Bibliothek zu nutzen. Die beiden Bibliotheken mit der höchsten Verbreitung sind dabei AssertJ und Hamcrest. Einen detaillierten Vergleich dieser beiden gab es bereits 2015 im Artikel „Ein Vergleich von Hamcrest, AssertJ und Truth“ von Marc Philipp hier im Heft.
Der größte und auch sichtbarste Unterschied besteht darin, dass bei Hamcrest die Assertions mittels statischer Methoden verschachtelt werden, wohingegen AssertJ auf ein Fluent-API setzt. Listing 8 zeigt diesen Unterschied.
Beide Ansätze funktionieren und sind sich dabei sehr ähnlich. Hamcrest ist etwas flexibler, beispielsweise bei der Negation von Prüfungen, aber es ist auch etwas schwerer, die passenden statischen Methoden zu finden, wenn wir nicht wissen, wonach wir suchen sollen. Bei AssertJ müssen wir uns hingegen nur den Einstiegspunkt merken, die möglichen Überprüfungen ergeben sich anschließend durch die zur Verfügung stehenden Methoden und sind mittels Code-Vervollständigung schnell zu sehen. Dafür lassen sich diese Überprüfungen nicht bei Nutzung kombinieren.
Genau an dieser Stelle setzt Deep Dive an. Wie AssertJ kommt es mit
einer Fluent-API daher, bietet uns aber, wie Hamcrest, die Möglichkeit,
Überprüfungen mittels not()
zu negieren. Zudem ist es uns möglich, mittels
back()
während der laufenden Überprüfung eines verschachtelten Objekts wieder
eine, oder mehrere, Ebenen hochzuspringen und dort weitere Überprüfungen
durchzuführen. Listing 9 zeigt, wie die Verwendung von Deep Dive aussieht.
Überprüfen von JSON
Wo wir gerade schon beim Thema Überprüfungen sind. Gerade wenn wir Dateiformate wie JSON oder XML überprüfen wollen, lohnt sich der Einsatz von spezifisch hierauf zugeschnittenen Bibliotheken. Natürlich können wir ein solches Format auch in einen String konvertieren und diesen vergleichen, allerdings riskieren wir hierbei, dass unser Test rot wird, wenn sich die Formatierung oder Reihenfolge ändert, und auch die Fehlermeldungen, die wir bei einem fehlschlagenden String-Vergleich erhalten, sind, wie in Listing 10, in der Regel wenig hilfreich.
Für JSON können wir hierfür zu JsonUnit greifen. Entweder wir nutzen das von
JsonUnit mitgebrachte, und an AssertJ angelehnte, assertThatJson
(s. Listing 11) oder wir nutzen Matcher in Kombination mit Hamcrest
(s. Listing 12).
Sehr angenehm finde ich dabei vor allem, dass JsonUnit es uns erlaubt, bei dem für die Prüfung benötigten JSON etwas ungenauer sein zu können. Listing 13 zeigt, dass wir auf die Anführungszeichen um Schlüssel verzichten können und einfache Anführungszeichen für String-Werte ausreichen. Beides macht die Tests lesbarer, weil wir nicht ständig Anführungszeichen mit einem Backslash escapen müssen.
Um Tests noch lesbarer zu machen, können wir die Überprüfung mittels Optionen noch erweitern. Dies ermöglicht es uns, beispielsweise nur ein spezifisches Subset von Feldern zu überprüfen und nicht jedes Mal das ganze Objekt komplett prüfen zu müssen (s. Listing 14).
Wenn uns diese Optionen noch nicht reichen, können wir noch zusätzliche spezielle Schlüsselwörter einsetzen oder sogar eigene Submatcher registrieren und nutzen. In Listing 15 nutzen wir mehrere der Schlüsselwörter und einen eigenen Matcher.
Zu guter Letzt haben wir auch noch die Möglichkeit, mittels JsonPath direkt bestimmte Elemente oder Werte zu überprüfen. Ist man im Spring-Umfeld unterwegs, gibt es außerdem JsonUnit-Module, um dieses in Tests für Spring MVC, das RestTemplate oder den WebClient bequem nutzen zu können.
Integrationstests für HTTP-Clients
In der Mitte der Testpyramide befinden sich Integrationstests. Diese testen im Gegensatz zu Unittests mehrere unserer Module im Zusammenspiel und beziehungsweise oder die Integration mit einer Schnittstelle außerhalb unserer Anwendung.
Bibliotheken, um HTTP-basierte Schnittstellen anzubinden, habe ich letztes Jahr bereits vorgestellt. Häufig kapseln wir eine solche Bibliothek innerhalb unserer Anwendung mit einer eigenen Klasse, welche die konkrete Anbindung realisiert. Um diese Klasse zu testen, haben wir nun zwei Möglichkeiten. Entweder wir mocken die eingesetzte HTTP-Bibliothek oder wir schreiben einen Integrationstest. Da externe Klassen nach Möglichkeit nicht gemockt werden sollten, halte ich hier den Integrationstest für die bessere Wahl. Hierzu können wir Hoverfly, Jadler oder Wiremock nutzen, um ad hoc in unserem Test einen HTTP-Server zu starten und auf spezifische Requests mit einer passenden Response zu antworten.
Jadler (s. Listing 16) bietet hierzu eine vom Testframework unabhängige Fluent-API an, bei der wir beim Matchen von Requests allerdings auch Hamcrest-Matcher nutzen können.
Hoverfly geht noch ein Stück weiter. Ähnlich wie Jadler können wir Hoverfly in unserem Test nutzen, um einen HTTP-Server zu starten. Für die Nutzung von JUnit 4 oder 5 steht uns aber bereits eine fertige Integration zur Verfügung (s. Listing 17). Wie wir sehen können, brauchen wir uns selbst nicht mehr um das Starten oder Stoppen zu kümmern und wir können uns voll auf die Definition von erwartetem Request und zu gebender Response konzentrieren.
Darüber hinaus können wir Hoverfly auch unabhängig von Java nutzen und Stand-alone als Server starten. Dies bietet sich vor allem dann an, wenn wir Hoverfly als Proxy zwischen uns und das wirkliche externe System setzen und den Capture-Modus anstellen. In diesem zeichnet Hoverfly unsere Requests und die gegebenen Antworten auf. Im Anschluss können wir Hoverfly anweisen, diese Aufnahme wieder abzuspielen.
Hoverfly erlaubt außerdem die Definition von Zustand und die Konfiguration während der Laufzeit via HTTP-Schnittstelle. Somit könnten wir in einem End-to-End-Test aus dem Testframework heraus die Interaktion mit dem gemockten externen System spezifizieren und müssen uns nicht auf eine statische Konfiguration verlassen.
Wiremock ähnelt vom Featureset her Hoverfly, bringt aber natürlich eine eigene Java-API mit.