Automatisches Testen des Controllers

https://bildung.social/@oerinformatik/

https://oer-informatik.de/sbb06_addresscontroller-test

tl/dr; (ca. 11 min Lesezeit): Es werden Unit-Tests für den REST-Controller erstellt. Dazu wird eine gemockte Umgebung konfiguriert. Das Testframework von Spring mit seinen zahllosen Klassen wird vorgestellt. Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt. Weiter geht es dann mit der Bestimmung der Testabdeckung mit Hilfe des Frameworks JaCoCo. (Zuletzt geändert am 04.09.2023)

Der soeben erstellte Controller soll nun um isolierte Unit-Tests ergänzt werden. Es wird hierbei im ersten Schritt nur getestet, ob die Requests den korrekten Controller erreichen und dieser die korrekten Methoden des Repositories aufruft. Da dieser Test nur den Controller und nicht die Datenhaltungsschicht betreffen soll, werden an Stelle der Objekte für die Datenspeicherung (v.a. das Repository) Doubletten gestellt, die deren Funktion nachahmen (mocken).

Um einen Überblick zu erhalten, welche Methoden wir testen sollten, werfen wir nochmal einen Blick auf das Klassendiagramm des Repositories:

Es sollten für folgende Methoden Tests generiert werden:

  • die vier CRUD-Methoden (Create/Read/Update/Delete):

    • create: post(new Address())

    • read: get(id)

    • update: put(address)

    • delete: delete(id)

  • Darüber hinaus gibt es noch:

    • list()

Erstellen einer Test-Klasse für den AddressController

In jeder IDE lassen sich die Test-Klassen anders erstellen, in VSCode beispielsweise, in dem man in der zu testenden Klasse die Befehlsübersicht aufruft (str-shift-p), dort “Java: Go to Test” …

und danach “Generate Testfile” auswählt:

Der korrekte Name und das Package werden schon vorausgewählt: kontrollieren, bestätigen!

Abschließend wir noch angeboten, für welche Methden Tests vorbereitet werden sollen:

Zunächst müssen wir die jUnit5-Integration in das Spring-Framework mit @ExtendWith(SpringExtension.class) ergänzen und die Testmöglichkeiten des MVC-Frameworks mit @WebMvcTest aktivieren. Daher müssen diese beiden Annotationen an der Klasse ergänzt werden:

Allein hierdurch werden eine Vielzahl an Imports nötig. Zum Abgleich: so müssen sie aussehen (und das ist nur der Anfang…):

Mocken des Repositories

Als Voraussetzung für den Test benötigen wir ein Umfeld (einen “Kontext”), in dem unsere HTTP-Requests aufgerufen werden. Über die Annotation @WebMvcTest verknüpfen wir dieses Umfeld. Der Kontext soll auf fingierte HTTP-Aufrufe reagieren. Das wiederum übernimmt ein Objekt der Klasse MockMvc. Über die Annotation @Autowired weiß das SpringBoot-Framework, dass es selbst die zugehörigen Instanzen dieser Klassen erzeugen und verknüpfen muss.

Um den Controller unabhängig von dem Repository und Model testen zu können, muss das Repository gemockt werden - also ein Testdouble erstellt werden, was keinerlei Logik enthält, sondern einfach bei Aufruf einer Methode einen zuvor definierten Rückgabewert übergibt. Hierzu verwenden wir das Tool Mockito. Dass wir das Repository simulieren wollen, hatten wir ja bereits über @MockBean private AddressRepository addressRepository; festgelegt.

Funktionsaufrufe nachahmen mit Mockito

Wie auf einen Funktionsaufruf reagiert werden soll können wir mit der Mockito-Methode when() festlegen (die unglücklicherweise so heißt wie unser given/when/then-Bereich in den Testmethoden, mit diesem aber nichts zu tun hat):

Die aufgerufene Methode kann hierbei direkt mit konkreten Parameterwerten genannt werden:

Wenn wir keine konkreten Parameter mocken wollen, sondern bei jedem Methodenaufruf den selben Rückgabewert simulieren wollen. kann dies mit any(KLASSENOBJEKT) angegeben werden. Konkret sieht das so aus:

Die einzelnen Testmethoden

Wir ergänzen für jede HTTP-Request-Methode einen ersten Test der überprüft, ob:

  • der korrekte HTTP-StatusCode,
  • der korrekte Inhaltstyp (JSON) und
  • der korrekte Inhalt

zurückgegeben wird.

Hierzu nutzen wir wieder die Aufteilung in given, when und then.

Im folgenden müssen wir den HTTP-Request mocken. Der Ablauf nutzt wieder das Builder-Pattern. Wer den Überblick behalten will, der kann gerne im angehängten UML-Klassendiagramm die Methoden und deren Rückgabewerte mitverfolgen.

Test der post()-Methode

Am Beispiel eines Tests für die POST-Methode zur Erzeugung neuer Ressourcen soll der Ablauf eines Tests exemplarisch dargestellt werden:

Im Ganzen sieht diese Test-Methode für POST folgendermaßen aus:

Die meisten Imports werden von den IDEs wieder gefunden, es gibt jedoch ein paar Knackpunkte, die oft händisch ergänzt werden müssen:

  • equalsTo() ist eine statische Methode von org.hamcrest.Matchers

  • post() (und später auch get() usw.) sind statische Methoden von org.springframework.test.web.servlet.request.MockMvcRequestBuilders

  • content(), status() sind statische Methoden von org.springframework.test.web.servlet.result.MockMvcResultMatchers

Im Ganzen sehen die Imports dann so aus:

Damit sollte es funktionieren. Leider reicht diese Erklärung nicht, um zu verstehen, was da genau passiert. Am Ende dieses Artikels habe ich mal einen Blick unter die Haube gewagt… aber erstmal wollen wir die Tests an laufen bekommen.

Ein guter Zeitpunkt, um schonmal zu schauen, ob der Test auch durchläuft. Menü: Anzeigen/Testen - und dort das “Play”-Symbol neben unserem gerade erstellten Test klicken:

List-Methode

Da die list()-Methode des Controllers auf addressRepository.findAll() zugreift müssen wir hier beim Mocken ansetzen.

Wir erzeugen den RequestBuilder über die get()-Methode und überprüfen, ob der Statuscode 200 (“OK”) ist. Darüber hinaus stellt das JSON-Objekt, dass wir als Antwort erhalten, ein Array dar, deshalb müssen wir bei der Inhaltsprüfung die jeweilige Zeile ansprechen: "$[0].firstName"

Die sonstige Struktur des post()-Tests bleibt weitgehend erhalten. Natürlich wieder im Dreisprung: given, when, then!

GET-Methode

Auch hier weichen nur die zu mockende Methode (findById()) und der RequestBuilder-Aufruf (get()) ab. Der Rest sollte uns bekannt vorkommen.

Hier wird noch der Import für Optional fällig:

PUT-Methode

Hier müssen zwei Methoden des Repositories gemockt werden: save() und existsById()

Delete

Bei delete() fallen die Tests deutlich schlanker aus, da wir bei Unit-Test zunächst nur den korrekten Aufruf der Repository-Methoden prüfen. Integrationstests wären hier im nächsten Schritt sicherlich angebracht, um das Zusammenspiel von Controller und Datenschicht zu testen.

Die Tests aufrufen

Nur kurz zur Wiederholung: Wir haben verschiedene Möglichkeiten, die Ausführung dieses Tests zu starten:

  • Im Projektexplorer im Abschnitt “Java Projekts” gibt es ein Play-Symbol neben dem Java-Filenamen des Tests

  • Wir rufen das entsprechende Maven-Goal auf: führe alles aus bis inklusive der Unittestphase mit allen Tests: shell mvn test -f ".\pom.xml"

  • Im Menü unter: Anzeigen/Testen findet sich ein Play-Symbol für die Tests (und später auch das Testergebnis)

Testergebnisse unter Anzeigen/Testen
Testergebnisse unter Anzeigen/Testen

Fazit

Die vier Testmethoden laufen durch. Es treten keine Fehler auf. Wunderbar.

Was haben wir gemacht? Getestet? Qualität gesichert? Sicher noch nicht. Wir haben die Technik ausprobiert, die wir zur Qualitätssicherung einsetzen werden. Bislang wurden weder allgemeine Testfälle formuliert noch wurden Testfälle systematisch ermittelt.

Bis hierhin kann allenfalls von einem rudimentären ersten Testfragment gesprochen werden: die eigentlichen Testfälle gemäß der Blackbox-Systematik sowie eine Messung der Testabdeckung mit entsprechender Ergänzung der Testfälle müssen nun folgen.

Exkurs: Was geschieht hinter den Kulissen - am Beispiel des post()-Tests?

Disclaimer: Der folgende Abschnitt erfordert etwas Konzentration und ein tieferes Verständnis von Objektorientierung. Sie können den Abschnitt überspringen, falls er mehr verwirrt als hilft.

In Springboot sind bei Operationen schnell sehr viele Klassen beteiligt, die jeweils nur eine Verantwortlichkeit haben (Single Responsibility Principle). Darunter leidet der Überblick, da nicht jeder Methodenaufruf direkt verrät, in welcher Klasse sich die Methode befindet. Das ist z.B. bei Methoden von statischen Imports der Fall (post()), zum Teil bei Methodenverkettungen (perform().andExpect()). Ich will hier einmal haarklein nachvollziehen, welche Methode in welcher Klasse steht, welche Datentypen als Parameter übergeben werden und welche Rückgabewerte sie haben. Hilfreich dazu ist v.a. die Dokumentation (JavaDoc) von SpringBoot, Spring und Java 17. Idealerweise bietet die IDE direkt die Links in die Dokumentation an.

Wiederholung: die `testPost()``-Methode

Wir haben oben eine Methode zum Testen von Post erstellt. Im //given-Teil ist das Mocken und das Erstellen des JSON-Strings sicher auch einen Blick wert, aber wir wollen uns zunächst auf alles, was ab //when passiert konzentrieren:

Aufrufe im //when-Abschnitt

Es geht also um folgenden Teil:

Wir analysieren diesen Aufruf mal mit Hilfe eines UML-Sequenzdiagramms:

UML-Sequenzdiagramm für die Erstellung des Requests
UML-Sequenzdiagramm für die Erstellung des Requests
  • Es wird eine statische Methode post() der Klasse MockMvcRequestBuilders aufgerufen. Durch den statischen Import kann der Aufruf abgekürzt werden (eigentlich: MockMvcRequestBuilders.post()). Diese Methode erzeugt und konfiguriert ein Objekt. Hierbei wird zur Objekterzeugung das Builder-Pattern genutzt: Typisch ist, dass erst das Objekt erstellt wird (new()), dann einzelne Attribute gesetzt werden (accept(), content(),…) ehe das konfigurierte Objekt zurückgegeben wird.

  • post() erzeugte also ein Objekt vom Typ RequestBuilder, anschließend wird diesers konfiguriert: Accept- und Content-Type-Header des Requests werden festgelegt sowie der eigentliche Request-Body als UTF-8-JSON-String übergeben. Jede dieser Konfigurations-Methoden gibt selbst wieder ein angepasstes MockHttpServletRequestBuilder-Objekt zurück, dadurch können sie in dieser Weise verkettet werden (Method-Chaining). Die Rückgabe-Datentypen lassen sich mit etwas Übung schnell aus dem UML-Klassendiagramm erkennen:

UML-Klassendiagramm um MockMvc, RequestBuilder und ResultActions
UML-Klassendiagramm um MockMvc, RequestBuilder und ResultActions
  • Etwas verwirrend in dem Zusammenhang ist, dass wir mit Hilfe des Builder-Pattern eine Objekt erstellen, das wiederum selbst ein Builder ist. MockMvcRequestBuilders baut ein Objekt vom Typ MockHttpServletRequestBuilder. Dieses Objekt wird später (im then-Abschnitt) den Request selbst bauen.

Den Überblick verloren? Nicht verzagen. Wichtig ist: wir haben im //when-Abschnitt mit testRequestBuilder ein Objekt erstellt, das unseren Request erstellen wird. Weiter geht’s.

Aufrufe im //then-Abschnitt

Der konfigurierte RequestBuilder wird als Parameter an mockMVC.perform() übergeben.

In der Methode perform() wird das eigentliche MockHttpServletRequest-Objekt vom Builder erstellt, der Request wird ausgeführt und das davon zurückgegebene response-Objekt wird gemeinsam mit dem Request in ein Objekt des Typs MvcResult gekapselt. Dieses Objekt wird per Dependency Injection in ein neu erstelltes ResultActions-Objekt übergeben:

Als Ergebnis erhalten wir ein ResultActions-Objekt, das uns erlaubt das Ergebnis über Methoden-Verkettung detailliert zu untersuchen. Jeder Aufruf von andExpect() gibt wiederum ein ResultActions-Objekt zurück:

Diese verketteten Methoden bilden die eigentlichen Zusicherungen, die wir in bisherigen Unit-Tests mit den assert*(*)-Methoden umgesetzt hatten. Ein Blick hinter die Kulissen zeigt auch, dass am Ende der Prozesse ein assertEquals()-Aufruf steht:

Es wird zunächst ein ResultMatcher-Objekt erzeugt und dieses wird der andExpect()-Methode übergeben. andExpect() ist eine Methode des ResultActions-Objekts des vorigen perform() bzw. andExpect()-Aufrufs und verfügt als solches bereits über die Abhängigkeit des MvcResult (darin enthalten das Request- und Response-Objekt).

Es ist nicht immer offensichtlich, zu welchen Klassen aufgerufene Methoden gehören, da die IDE einige statische Imports vornimmt. Um beispielsweise herauszufinden, an welcher Stelle status(), andExpect() oder isCreated() implementiert werden lohnt ein Blick in die Sourcen, in JavaDoc oder in die Zusammenfassung in diesem UML-Klassen-Diagramm:

Manchmal ist etwas kriminalistisches Gespür von Nöten, um herauszufinden, was da genau geschieht. Es trägt jedoch zum sicheren Umgang mit dem Framework bei, wenn man auch immer mal der Neugier nachtgibt und hinabtaucht in das, was wie Zauberhand aussieht.

Nächste Schritte

Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt.

Weiter geht es dann mit der Bestimmung der Testabdeckung mit Hilfe des Frameworks JaCoCo.


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY-SA 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: Automatisches Testen des Controllers” von Hannes Stein, Lizenz: CC BY-SA 4.0. Der Artikel wurde unter https://oer-informatik.de/sbb06_addresscontroller-test veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Stand: 04.09.2023.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: