API Dokumentation mit spring-boot-docs

Documentation is a love letter that you write to your future self.

Damian Conway

Es gibt die verschiedensten Arten die eigene Rest API zu dokumentieren, Tools wie Swagger können da helfen oder man schreibt selbst Markdown oder Asciidoc Dokumente. Wenn die Swagger Beschreibung nicht zur Generierung der API genutzt wird, dann kann man nie sicher sein, dass die Dokumentation auch wirklich die Realität beschreibt.

Bei der selbst geschriebenen Dokumentation hat man ein ähnliches, weit verbreitetes Problem. Die Dokumentation muss für jede minimale Änderung an der API korrigiert werden. Wer die Aktualität von Quellcode Kommentaren kennt, die ja sogar direkt im Quellcode stehen, weiß um die Sorgfalt der Entwickler für andere Arten von Dokumentation. Ein alte Regel besagt, Dokumente sind schon nach dem ersten Tag veraltet und fehlerhaft.

Die Autoren von spring-boot-rest bieten den Software Entwicklern eine interessante Alternative. Ein Mittelweg von Selbstgemacht und Generiert.

Dafür schreibt der Entwickler seine Dokumentation mit Asciidoctor und fügt Fragmente für beispielhafte Rest Aufrufe per include in die Dokumentation ein.

Die Fragmente können die Darstellung von CURL Aufrufen, Parametern, Links,  HTTP-Requests und Responses oder nur deren Inhalte im Asciidocter Format enthalten.

Wo kommen aber nun die Beispiel Fragmente her? Hier zeigt sich die Eleganz der Lösung, denn sie stammen aus einer Quelle, bei der die Entwickler sicher sein können, dass sie aktuell und fehlerfrei sind. Sie stammen aus den selbst geschriebenen Spring Boot Unit Test.

Um die Fragmente zu erzeugen, benötigen wir die beiden Artefakte spring-boot-docs und spring-restdocs-mockmvc.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-docs</artifactId>
  <version>2.1.3.RELEASE</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.restdocs</groupId>
  <artifactId>spring-restdocs-mockmvc</artifactId>
  <scope>test</scope>
</dependency>

Die Annotationen der Test Klasse müssen um den Support für spring-boot-doc erweitert werden.

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith({ RestDocumentationExtension.class, SpringExtension.class })
public class AncestorControllerDocTest {
...
}

Da in der Dokumentation für den Server nicht localhost erscheinen soll, sondern ein echt klingender Name, ergänzen wir die Testklasse um eine setUp Methode für unser MockMvc Objekt.

private MockMvc mvc;

@BeforeEach
public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
  mvc = webAppContextSetup(webApplicationContext).apply(documentationConfiguration(restDocumentation).uris()
    .withScheme("https").withHost("ancestors.schegge.de").withPort(443)).build();
}

Auch ein schönes Beispiel für die Möglichkeit von JUnit 5 Parameter in die annotierten Methoden zu injizieren. Die Tests entsprechen den üblichen JUnit 5 Tests für Spring Boot. Angereichert werden sie jedoch mit zusätzlichen Dokumentationscode in der andDo Methode.

@Test
void testAnchestor1208699983() throws Exception {
  mvc.perform(get("/ancestor/{organisation}/{identifier}", "gebdas", "1208699983").contentType(APPLICATION_JSON))
    .andExpect(status().isOk())
    .andDo(document("ancestors/single", 
      links(halLinks(),
        linkWithRel("mother").optional().description("Link zu den Personendaten des Vaters"),
        linkWithRel("father").optional().description("Link zu den Personendaten der Mutter"),
        linkWithRel("husband").optional().description("Link zu den Personendaten des Vaters"),
        linkWithRel("wife").optional().description("Link zu den Personendaten der Mutter"),
        linkWithRel("child").optional().description("Link zu den Personendaten eines Kindess"),
        linkWithRel("self").description("Link zu den Personendaten der Person")),
        pathParameters(
          parameterWithName("organisation").description("Die Organisation, von der diese Personendaten stammen"),
          parameterWithName("identifier").description("Der Identifier bei der Organisation"))));
}

Es gibt eine lange Liste von Methoden, mit denen die Dokumentation angepasst  werden kann. In unserem Beispiel werden nur die Links und die PathParameter beschrieben. Es werden die Links und PathParameter nicht nur beschrieben, sondern auch geprüft. Unbekannte oder fehlende Werte führen zu einem Testfehler.

Läuft unser Test durch, dann benötigen wir noch ein Asciidoctor Dokument, mit dem wir die erzeugten Fragmente zusammenfügen können.

= Ahnenregister API

== Alle Personen

== Eine Person

Eine Person holt man aus dem Ahnenregister mit

include::{snippets}/ancestors/single/http-request.adoc[]

include::{snippets}/ancestors/single/path-parameters.adoc[]

include::{snippets}/ancestors/single/http-response.adoc[]

Die Verlinkungen zwischen den Personendaten können folgende Relationen enthalten.

include::{snippets}/ancestors/single/links.adoc[]

Mit Asciidoctor bearbeitet ergibt sich z.B. die folgende HTML Dokumentation

Auf den ersten Blick etwas viel Arbeit für einen einzelnen Request. Aber bei dutzenden von REST-Request, die sich womöglich während der Entwicklung auch noch ändern, und vielen gemeinsam genutzten Fragmenten, ist die automatisierte Generierung der  REST Dokumentation eine immense Zeitersparnis und Arbeitserleichterung.