29
.
11
.
2016

Dokumentation mit Spring REST Docs

Dokumentation mit Spring REST Docs

In diesem Blog-Post werden wir die REST-Api der WebApp aus dem letzten Post mit den Spring REST Docs dokumentieren.

Warum Spring REST docs?

Zugegeben es kostet Zeit, das erste mal Spring REST Docs einzurichten und zu verwenden. Doch mit einer detaillierten Anleitung gelingt dies deutlich schneller als eine Dokumentation von gleicher Qualität von Hand zu erstellen. Hat man dann erst einen Endpoint fertig sind 90% der Arbeit nur noch copy and paste mit kleinen Anpassungen, und man kreiert in Minuten hochwertige Dokumentationen für weitere Endpoints. Sollte sich dann etwas am Code ändern muss man nur wenig oder gar nichts anpassen um die Dokumentation aktuell zu halten.

Funktionsweise:

Die Endpoints werden mit dem Spring-Test-Framework gemocked und die Anfragen und Rückgaben mit den Klassen von Spring REST Docs dokumentiert. Aus den erlangten Daten werden dann Snippets in asciidoc syntax erstellt welche in eine Dokumentation derselben Syntax eingefügt werden.

Projekt-Specs

Ich verwende eine von mir geschriebene Web-App welche hier https://github.com/espendennis/mitfahr-zentrale gefunden werden kann. Es handelt sich hierbei um eine MitfahrApp bei der man user erstellen und Angebote einstellen kann. Sie hat eine REST-Api für welche wir mit Spring REST Docs eine Dokumentation erstellen werden.

Es wurden Spring-Boot, Maven und eine H2-Datenbank für die Tests verwendet. Getestet wird mit dem Spring MVC Test Framework. Für die Erstellung der Dokumentation werden wir Asciidoctor verwenden. Wir werden die Dokumentation für das Offer-Model schreiben.

Das Offer-Model

https://github.com/espendennis/mitfahr-zentrale/blob/master/src/main/java/com/espen/ws/model/Offer.java

Der REST-Endpunkt

https://github.com/espendennis/mitfahr-zentrale/blob/master/src/main/java/com/espen/ws/web/api/OfferController.java

Der erste Test

Als erstes erstellen wir eine neue Test-Klasse die wir OfferApiDocumentation.java nennen. Dadurch schaffen wir eine Abgrenzung zwischen der Dokumentation und den Tests. Dadurch bleibt der Code übersichtlich und strukturiert. Wichtig ist bei der Benennung nur, dass sie auf Documentation endet, da wir diese später mit surefire beim build starten werden. Da diese Tests nur für die Dokumentation sind, können diese sehr simpel gehalten werden. Als erstes schreiben wir einen Test um eine neue Offer zu posten.

github:c1de921ec529008453c4aac7af83b9ad

Setup

Nun müssen wir das Projekt für Spring REST Docs vorbereiten. Dafür müssen wir einiges in unserer pom.xml hinzufügen. Als erstes fügen wir die nötige Dependency hinzu.

Dank Spring-boot können wir die gemanagte Version verwenden.

github:a2d84be034a278dd36f902175b641ef7

Nun müssen wir ein property setzen um den Ausgabepfad der erstellten Snippets anzugeben

github:c4223e1a7b80aa6579f8a85afd858862

und plugins hinzufügen

github:2094214474d2d41ce57f3ec7aa6c5e28

Hier wird das maven surefire plugin hinzugefügt und so konfiguriert, dass alle Tests welche nach dem Muster “**/*Documentation.java” benannt wurden eingebunden werden.

Danach folgt das Asciidoctor-plugin. Die Einstellung <phase>prepare-package</phase>

sorgt dafür dass die Dokumentation in der prepare-package phase erstellt wird und so mit ins package gepackt und dann in der fertigen Web-Application verlinkt werden kann.

Die ersten Ausgaben erzeugen

Nun fügen wir Code in unsere Testklasse ein, welche die ersten Snippets erstellt.

Notwendige Imports:

github:8a802f8d1c59c19aec382cbf058eaa19

Klasse

github:46c8240337d8b94e8a91e383b40ad7f5

Die Zeile .andDo(document("index")); ruft die statische Methode document() der Klasse MockMvcRestDocumentation auf und dokumentiert die Daten aus der ausgeführten Anfrage im in der pom.xml festgelegten Ordner im Unterordner /index.

Das Resultat sind 4 Snippets im .adoc Format im Ordner “/target/generated-Snippets/index”

curl-request.adoc

http-request.adoc

http-response.adoc

httpie-request.adoc

Als Beispiel die http-response.adoc:

github:deb278a2dc71f833ecf6d272150b327c

Beschriftungen hinzufügen

So weit so gut. Damit der User nun etwas mit diesen Daten anfangen kann, sollten wir Beschreibungen für die Felder hinzufügen. Dazu übergeben wir der document()-Methode Objekte, welche diese Beschreibungen enthalten. Wenn dies gemacht wird, müssen alle Felder dokumentiert werden, sonst schlägt der Test fehl.

Notwendige Imports:

github:599e0ec734b3734d13001f77d5cdb729

Der Code:

github:9b79fb67eeaca59c0cd744d5908fbffd

Das Ergebnis davon ist jetzt eine zusätzliche Datei in unserem Snippets-Ordner:

response-fields.adoc

github:112327954e3a86be7a918217e61c1a5f

Diese enthält eine Tabelle, welche die Felder des Offer-Models dokumentiert und neben dem Typ auch die eben hinzugefügten Beschreibungen enthält. Nach einem Klick auf raw wird der Code in asciidoctor Syntax angezeigt.

Beschriftungen wieder verwenden

Dies umfasst aber erst nur die Felder der Antwort. Die Anfrage selbst ist damit noch nicht dokumentiert. Um Mengen an repetitiven Code zu vermeiden gibt es eine elegantere Lösung für dieses Problem. Es können FieldDescriptor-Arrays erzeugt werden, welche dann einfach so oft wie notwendig der document-Methode übergeben werden können.

Notwendige Imports

github:abf8e921abd084a94a56da6b947340f9

Erst müssen wir das FieldDescriptor[] für das Offer-Model erstellen

github:dee85648aceb9a03fdab810451d9ae95

und dann dieses statt der einzelnen responsefields übergeben:

github:80c7dce5994bc16c29046ec108db1d94

Die Ausgabe bleibt dieselbe, doch müssen die descriptions nur einmal geschrieben werden und können dann so oft wie wir wollen referenziert werden. So haben wir nun auch ein Snippet für die request-fields

Ausgaben formatieren

Zugegeben sieht das Ergebnis noch nicht sehr schön aus. Deshalb werden wir printpretty verwenden um die JSON-Daten ansehnlicher zu gestalten.

Notwendige Imports

github:8266898e9fba6b98e0e7ea42ffc71d43

Erst müssen wir einen RestDocumentationResultHandler hinzufügen

github:1daefc858ce1ac5d8f54659a4c4a362f

und diesen dann in der init-Methode instanziieren. Durch den Parameter “{method-name}” wird spezifiziert, dass nicht mehr im Unterordner /index gespeichert werden soll, sondern in einem Unterordner mit dem Namen der jeweiligen Test-Methode. Dies ist sehr wichtig damit nicht mit jedem Test die vorherigen Snippets überschrieben werden. Zudem setzen wir prettyPrint() als preprocessor für Anfragen und Antworten. Jetzt muss nur noch der MockMvc Instanz mit .alwaysDo(this.document) gesagt werden, dass sie diesen result handler vor jedem Test aufrufen soll.

github:45965f70f6ce7de388125ee29b361f6a

Jetzt müssen wir noch die Test-Methode anpassen um den neuen ResultHandler aufzurufen:

github:9f84c22f7c7e23e473d3e834320a0325

Nun sieht das Ergebnis schon viel besser aus

github:486b1b6f4400a1bef962677dc558970c

Die Dokumentation zusammensetzen

Nun wird es Zeit eine Dokumentation zu erstellen in der wir diese Snippets verwenden und das Ergebnis unserer Arbeit betrachten können:

Dafür erstellen wir einen neuen Ordner in src/main den wir asciidoc nennen und darin eine Datei mit dem Namen OfferDocumentation.adoc. In dieser Datei erstellen wir eine Dokumentation in ascii-doctor Syntax für unser Offer-Model. Wer damit noch nicht gearbeitet hat, sollte einen kurzen Blick in die asciidoctor Dokumentation werfen. Sehr behilflich ist das Chromeplugin https://chrome.google.com/webstore/detail/asciidoctorjs-live-previe/iaalpfgpbocpdfblpnhhgllgbdbchmia. Damit kann die erzeugte Dokumentation im Browser gerendert werden und man sieht immer wie das Endergebnis aussieht.

Es ist immer gut am Anfang mit den verwendeten Http-verbs und Http-status-codes einzuleiten und dann auf die verschiedenen Anfragen und Rückgaben einzugehen.

Mit include::{Snippets}/post-offer/request-fields.adoc[] können die Snippets in das Dokument eingefügt werden. Der Pfad ist relativ zu unserem OfferDocumentation.adoc anzugeben. Durch unsere Einstellungen in der pom.xml ergibt sich dann nach dem build obiger Pfad. Um beim Erstellen mit dem Chrome-plugin die Snippets auflösen zu können um das komplette Ergebnis zu sehen, müssen die Snippets erst manuell in diesen relativen Pfad zur OfferDocumentation.adoc kopiert werden.

Das Ergebnis:

github:b4ab9bba1ccd4617f708285d6bea6ab2

Mit einem Klick auf RAW kann der sourcecode in asciidoc syntax gesehen werden.

Implementierung der restlichen Endpoints

Nun können wir die restlichen Endpoints implementieren. Dabei gibt es leider noch ein paar Stolpersteine

Für die Requests mit den Methoden put, get und delete können nicht die üblichen Requestbuilder verwendet werden, sondern müssen durch spezielle Requestbuilder aus dem restdocs-Packet ersetzt werden:

github:4b1fe3396dbc157bc10480dc8698ac75

Es gibt Anfragen, welche ein Array an Objekten zurückgeben. Hier ist zu sehen wie diese zu dokumentieren sind

github:9f675142e54eb9c9bf980cd8d8323efd

Manche Anfragen benötigen einen Pfadparameter

Hier ist zu sehen, wie diese zu dokumentieren sind

Notwendige Imports

github:9c069ceabf1175e361cb03637deed7b6
github:e827bd176199e2722f82748eab507bb6

Zusätzliche Snippets einfügen

Nun können die Snippets in die OfferDocumentation.adoc eingefügt werden

github:a66a799feca2241d3cbecfe810547fee

Constraints

Ein sehr wichtiger Punkt fehlt jetzt noch. Wenn ein User Anfragen an die API schickt muss er die Constraints der Modelle kennen. Sonst kann er keine passenden Anfragen stellen.

Um repetitiven Code zu vermeiden erstellen wir hierfür eine statische innere Klasse

github:d9415708ea38622cc4512fa39da4f88f

Diese enthält ein Feld mit einer ConstraintDescription und einen Konstruktor an welchen die Klasse des Modells übergeben wird, für welches wir die Constraints dokumentieren wollen. Zudem enthält die Klasse die Methode FieldDescriptor withPath(String path). Path ist hier das Feld des Models welches beschrieben werden soll. Diese Methode gibt einen FieldDescriptor zurück an welchen zuerst mit Hilfe der ConstraintDescription Attribute für die constraints angehängt werden.

Diese Klasse muss nun instanziiert werden. Dabei geben wir an für welche Klasse die Constraints dokumentiert werden sollen.

github:80d49473cc9ea4a917e01be30fc32b8b

Nun verwenden wir diese Klasse um die nötigen Attribute an unser FieldDescriptor[] anzuhängen

github:9b6db166cabaa9256bf77c8d71efa561

Zuvor haben wir Standard FieldDescriptors erzeugt, ihnen dabei einen Pfad(Das Feld des Modells welches wir beschreiben wollen) übergeben und eine description angefügt. Jetzt erzeugen wir in der statischen Methode withPath() einen FieldDescriptor, übergeben wieder den Pfad, hängen Attribute für die Constraints an und geben diesen zurück. Dann verwenden wir ihn genauso, wie die Standard-Version zuvor und hängen die description an.

Die nun hinzugefügten Constraints brauchen jetzt noch ihren Platz im Template für die Snippets, weswegen wir ein eigenes erzeugen müssen.

Damit das StandardTemplate überschrieben wird muss unser Template in den Ordner src/test/resources/org/springframework/restdocs/templates/asciidoctor/ und muss request-fields.Snippet heißen. Wir kopieren nun einfach das original und fügen eine Spalte für die Constraints an.

github:e2ca95ae5ba51b1ae50b9485bbff7d3b

An unserer OfferDocumentation.adoc muss nichts geändert werden. Das geänderte Snippet wird automatisch inklusive der neuen Spalte eingefügt.

Die Dokumentation builden

Nun müssen wir noch maven mit dem Goal package aufrufen und unsere Dokumentation wird als html-file in den Ordner target/generated-docs gespeichert.

Hier zum Abschluss der fertige Code:

Die fertige Test-Klasse:

https://github.com/espendennis/mitfahr-zentrale/blob/master/src/test/java/com/espen/documentation/OfferApiDocumentation.java

Die fertige .adoc:

https://github.com/espendennis/mitfahr-zentrale/blob/master/src/main/asciidoc/OfferDocumentation.adoc

Dennis
Junior Software Engineer - Java, JVM

Leidenschaft, Freundschaft, Ehrlichkeit, Neugier. Du fühlst Dich angesprochen? Dann brauchen wir genau Dich.

Bewirb dich jetzt!