1. Public API und Zweck der Dokumentation
Wenn man mit Enthusiasmus an REST Docs herangeht: „Lasst uns doch einfach alles dokumentieren, was der Server zurückgibt“, dann verwandelt man die Dokumentation schnell in ein Museum der Zufälligkeiten. Gestern haben Sie das Feld debugInfo „mal kurz“ hinzugefügt, heute vergessen es zu entfernen, morgen hat sich der Kunde erfreut darauf verlassen, und dann löschen Sie es – und schon haben Sie eine neue Staffel der Serie „Breaking Changes: The Reckoning“.
Daher beginnen wir in ContentHub mit der public surface: das, was externe Clients (oder zumindest andere Teile Ihres Systems) tatsächlich konsumieren. Das sind vor allem GET /api/public/articles und GET /api/public/articles/{slug}, plus die obligatorischen Begleiter jeder öffentlichen API: Paginierung und Fehler. Interne Details des Dienstes, „wie wir dort drinnen Artikel aus der Datenbank auswählen“, sollen REST Docs nicht nacherzählen. Dokumentation ist ein Vertrag an der HTTP-Grenze, keine Autobiografie Ihres @Service.
Um nicht in „alles Mögliche dokumentieren“ abzudriften, ist es nützlich, eine einfache (und sehr unromantische) Prioritätentabelle im Kopf zu behalten:
| Was wir dokumentieren | Warum ist das für den Kunden wichtig | Typische Folgen, wenn nicht dokumentiert |
|---|---|---|
| Public list/details Endpunkte | Dies ist der Haupteinstieg in das System | Der Kunde schreibt Code „nach Gefühl“ und beginnt, den Vertrag zu erraten |
| Query-Parameter der Paginierung/Sortierung/Filter | Der Kunde muss Format und Standardwerte kennen | Endloses „warum beginnt page bei 0?“, „wie macht man sort?“ |
| Page Wrapper (Seiten-Metadaten) | Der Kunde baut die UI-Paginierung | UI geht kaputt, weil totalPages plötzlich verschwunden ist |
| Fehlervertrag (ApiProblem) | Fehler sind auch Teil der API | Der Kunde kann 404 und 409 nicht unterscheiden, zeigt keine Verstöße an |
Die Idee der Lektion: Wir dokumentieren sparsam, aber streng nach Sinn. Das heißt, wir beschreiben wichtige Felder, Parameter und Strukturen, aber machen keinen vollständigen JSON-Snapshot „wie die Serialisierung heute gerade will“.
2. Query-Parameter GET /api/public/articles
Paginierung und Filter werden oft als „das sind doch Kleinigkeiten, der Kunde errät es selbst“ wahrgenommen. Und dann sehen Sie im Team-Chat: „Leute, ist page bei uns nullbasiert oder einsbasiert?“, „Ist size begrenzt?“, „Wird sort per Komma oder Leerzeichen getrennt?“. Und schon diskutieren Sie die API nicht anhand der Dokumentation, sondern nach dem Gedächtnis der Leute. Das Gedächtnis der Leute ist eine kreative Sache, also vertrauen Sie ihm besser nicht.
REST Docs ist hier gut, weil es uns zwingt, den Eingangsvertrag ehrlich zu beschreiben: welche Query-Parameter gibt es, was bedeuten sie, welche sind optional, welche Standardwerte haben sie. Und vor allem – wir beschreiben es im Test, der bereits prüft, dass der Endpunkt mit 200 OK antwortet und JSON in korrekter Form zurückgibt.
Das Testgerüst für REST Docs sieht normalerweise so aus (wir bleiben im MVC-Slice, weil wir die HTTP-Grenze dokumentieren, nicht die Datenbank):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
// MVC-Slice-Test: nur die Webschicht hochfahren, um den HTTP-Vertrag zu prüfen
@WebMvcTest(PublicArticleController.class)
@AutoConfigureRestDocs // Aktiviert Snippet-Generierung in build/generated-snippets
class PublicArticleRestDocsTest {
@Autowired
MockMvcTester mvc; // Vereinfachter Client zum Ausführen von HTTP-Anfragen an MockMvc
}
Jetzt fügen wir dem Test genau die Dokumentation der Query-Parameter hinzu. Beachten Sie den Stil: Zuerst prüfen wir den Status, und erst dann „hängen“ wir document(...) an. Das ist psychologisch wichtig: Die Dokumentation ist nicht das Hauptziel des Tests, sie ist sekundär zur Überprüfung des Vertrags.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentPublicArticlesQueryParams() {
// Führe einen echten (für den Test) HTTP-Aufruf zum Controller durch
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20&sort=publishedAt,desc"))
// 1) Zuerst prüfen wir, dass der Happy Path wirklich happy ist
.hasStatusOk()
// 2) Erst danach generieren wir die Dokumentations-Snippets
.apply(document("public-articles-list",
queryParameters(
// Beschreiben den Eingangsvertrag: welche Parameter der Endpunkt akzeptiert
parameterWithName("page").optional().description("Seitennummer (0-basiert)"),
parameterWithName("size").optional().description("Seitengröße"),
parameterWithName("sort").optional().description("Sortierung im Format field,(asc|desc)")
)
));
}
Ein paar wichtige Nuancen, die meist nicht sofort, sondern nach der ersten „Schlacht mit der Realität“ auftauchen.
Erstens, optional() ist hier nicht einfach „na ja, es ist optional“. Es geht auch darum, dass Sie den Test möglicherweise ohne diesen Parameter ausführen (z. B. Standard page=0, size=20). Wenn Sie optional() vergessen, erwartet REST Docs den Parameter immer und beschwert sich, wenn Sie den Endpunkt ohne sort aufrufen. Die Dokumentation muss den realen Vertrag widerspiegeln: Ein Parameter ist optional – also ist er optional.
Zweitens, Beschreibungen in description(...) sollte man so schreiben wie für einen Kunden, der die API zum ersten Mal sieht. Der Satz „page param“ ist fast wie im Code int x; // x zu schreiben. Formal gibt es einen Kommentar, aber praktischer Nutzen – null.
Um die Dokumentation anschaulicher zu machen, ist es oft praktisch, eine „interne Tabelle“ für die Paginierungsparameter im Kopf zu behalten (in REST Docs wird sie nicht direkt eingefügt, hilft aber, die Deskriptoren richtig zu formulieren):
| Parameter | Beispiel | Bedeutung | Beantwortet die Frage: |
|---|---|---|---|
| page | 0 | Seitennummer | „Nummerierung ab 0 oder ab 1?“ |
| size | 20 | Seitengröße | „Welcher Standard? Gibt es ein Limit?“ |
| sort | publishedAt,desc | Sortierung | „Wie gibt man die Sortierrichtung an?“ |
Wenn GET /api/public/articles Filter hat, z. B. category, muss dieser ebenfalls dokumentiert werden – sonst sucht der Kunde auf archäologische Weise nach dem „magischen Parameter“:
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentCategoryFilter() {
// Szenario: Der Kunde filtert die Artikelliste nach Kategorie
assertThat(mvc.get().uri("/api/public/articles?category=java&page=0&size=20"))
.hasStatusOk() // Wichtig: Der Filter darf die erfolgreiche Antwort nicht „zerstören“
.apply(document("public-articles-list-with-category",
queryParameters(
// Filter sind auch Teil des öffentlichen Vertrags
parameterWithName("category").optional().description("Filter nach Kategoriecode"),
// Paginierung bleibt gleich
parameterWithName("page").optional().description("Seitennummer (0-basiert)"),
parameterWithName("size").optional().description("Seitengröße")
)
));
}
Ja, Snippets werden sich vermehren. Und das ist in Ordnung, solange sie nicht zu „ein Snippet für jeden Nieser“ werden. Der Sinn liegt nicht in der Anzahl der Dateien, sondern darin, dass jedes Snippet ein separates stabiles Szenario abdeckt.
3. Paginierungsantwort: Wrapper und content[]
Die paginierte Antwort ist der Punkt, an dem sich „scheinbar alles klar“ in „warum kann auf dem Client keine vernünftige Paginierung gemacht werden“ verwandelt. Der Kunde braucht normalerweise zwei Dinge: eine Liste der Elemente (bei uns die Artikel) und Metadaten der Seite (wo wir sind, wie viele Seiten insgesamt, welche Größe). Wenn die Metadaten nicht dokumentiert werden, beginnt jeder Kunde, Ihre API nach eigenem Verständnis zu interpretieren.
In ContentHub geben wir nicht direkt Page<Article> nach außen (und das ist richtig), sondern verwenden einen Wrapper wie PageResponse. Daher sollte REST Docs idealerweise die Struktur des Wrappers und die Struktur des Listenelements separat zeigen. Und hier ist es wichtig, nicht alles ohne Verständnis in einen Topf mit fieldWithPath("content[].something") zu werfen.
Beginnen wir mit einer grundlegenden Dokumentationsbeschreibung von PageResponse, ohne auf die Felder des Elements einzugehen. Es ist, als würde man eine Box mit „hier liegt eine Ergebnisseite“ beschriften und angeben, dass sich darin content, page, size, totalElements, totalPages befinden.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentPublicArticlesPageWrapper() {
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
.hasStatusOk() // Prüfen, dass die Paginierung grundsätzlich funktioniert
.apply(document("public-articles-list-page",
responseFields(
// Beschreiben den Wrapper der Seite (Metadaten + Liste)
fieldWithPath("content").description("Liste der Artikel auf der Seite"),
fieldWithPath("page").description("Seitennummer (0-basiert)"),
fieldWithPath("size").description("Seitengröße"),
fieldWithPath("totalElements").description("Gesamtzahl der gefundenen Artikel"),
fieldWithPath("totalPages").description("Gesamtzahl der Seiten")
)
));
}
Jetzt dokumentieren wir die Elemente content[]. Und genau hier beginnen viele, einen „vollständigen Schnappschuss der Artikelkarte“ mit allen Feldern zu machen, einschließlich solcher, die der Kunde gar nicht braucht. In der öffentlichen Liste reicht normalerweise das, was tatsächlich in der Liste angezeigt wird: slug, title, summary, publishedAt, etwas zur Kategorie.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentArticleCardInList() {
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
.hasStatusOk()
.apply(document("public-articles-list-content",
responseFields(
// Felder, die der Kunde normalerweise in der Liste anzeigt
fieldWithPath("content[].slug").description("Öffentlicher Artikelidentifikator"),
fieldWithPath("content[].title").description("Artikeltitel"),
fieldWithPath("content[].summary").description("Kurzbeschreibung"),
fieldWithPath("content[].publishedAt").description("Veröffentlichungszeit"),
// Mindestmetadaten der Seite, damit die UI die Paginierung aufbauen kann
fieldWithPath("page").description("Seitennummer (0-basiert)"),
fieldWithPath("size").description("Seitengröße")
)
));
}
Sie haben bemerkt, dass wir in diesem Snippet wieder page und size dokumentieren. Das sieht nach Duplizierung aus – und das ist es auch. Aber in REST Docs gibt es eine Balance: Manchmal ist es einfacher (und lesbarer), ein paar Felder leicht zu duplizieren, als zu clevere Frameworks und „universelle Snippets für alle Lebenslagen“ zu bauen.
Wenn man es sauberer haben möchte, kann man die gemeinsamen FieldDescriptor in eine Konstante auslagern und wiederverwenden. Das ist besonders nützlich, wenn dieselben Paginierungs- oder ApiProblem-Felder in mehreren Snippets wiederholt werden.
Ein weiterer praktischer Punkt: Wenn Ihre API sich weiterentwickelt und Sie neue Felder in die Antwort aufnehmen (z. B. content[].readingTimeMinutes), wird das strenge responseFields(...) verlangen, jedes Feld zu beschreiben. Das ist gut, wenn Sie eine strenge Kontrolle wünschen, aber manchmal muss nur das stabile Minimum dokumentiert werden. Dann ist die „entspannte“ Variante relaxedResponseFields(...) nützlich, die nicht fehlschlägt, wenn es undokumentierte Felder gibt.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentOnlyStableFields() {
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
.hasStatusOk()
.apply(document("public-articles-list-relaxed",
relaxedResponseFields(
// Dokumentieren nur den „stabilen Kern“, den Rest ignorieren
fieldWithPath("content[].slug").description("Öffentlicher Artikelidentifikator"),
fieldWithPath("content[].title").description("Artikeltitel")
)
));
}
Das ist kein „Cheaten“. Es ist ein bewusster Kompromiss: Sie wählen, was Ihnen wichtiger ist – strikte Vollständigkeit der Dokumentation oder Stabilität gegenüber dem Hinzufügen nebensächlicher Felder. Für eine öffentliche API ist es meist besser, die Dokumentation nah am Vertrag zu halten und den Vertrag stabil zu halten. Aber wenn Sie gerade erst mit REST Docs anfangen, kann der Relaxed-Modus die Psyche retten.
4. Details-Endpunkt: GET /api/public/articles/{slug}
Es gibt die Versuchung, list und details mit einem Snippet zu dokumentieren, weil „es ist ja ein Artikel, was macht das für einen Unterschied“. Der Unterschied ist zumindest, dass details normalerweise mehr Daten enthält, während list kompakter ist. Und wenn Sie sie in eine „universelle Artikel-Dokumentation“ mischen, wird der Kunde versuchen, list als details zu verwenden oder umgekehrt, und dann beginnt das klassische „warum ist body immer null?“.
Daher machen wir ein separates Snippet für den Details-Endpunkt. Und sofort taucht ein weiteres Detail des Vertrags auf: der Path-Parameter slug. Selbst wenn Sie pathParameters(...) nicht verwenden (und das gibt es in REST Docs), sollte aus dem Snippet bereits hervorgehen, dass slug der Schlüssel der Anfrage ist und was genau dort eingesetzt werden muss.
Beginnen wir mit der Dokumentation der Details-Antwort:
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentPublicArticleDetails() {
// Szenario: Der Kunde öffnet die Seite eines bestimmten Artikels
assertThat(mvc.get().uri("/api/public/articles/java-testing-basics"))
.hasStatusOk() // Sichergestellt, dass der Artikel verfügbar und der Endpunkt lebendig ist
.apply(document("public-article-details",
responseFields(
// Details enthält normalerweise mehr als die List-Card, aber wir dokumentieren nach Sinn
fieldWithPath("slug").description("Öffentlicher Artikelidentifikator"),
fieldWithPath("title").description("Artikeltitel"),
fieldWithPath("summary").description("Kurzbeschreibung"),
fieldWithPath("body").description("Volltext des Artikels"),
fieldWithPath("publishedAt").description("Veröffentlichungszeit")
)
));
}
Hier gilt wieder die Sparsamkeitsregel: Man muss nicht „alle Felder des Domänenmodells“ beschreiben, wenn sie nicht Teil des stabilen öffentlichen Vertrags sind. Zum Beispiel version (optimistic Lock) betrifft den Kunden der öffentlichen API überhaupt nicht. Was Ihnen in der Datenbank und JPA wichtig ist, ist nicht unbedingt für den externen Konsumenten wichtig. Die Dokumentation hilft Ihnen, diese Vermischungen zu bemerken: Wenn die Hand danach greift, version und authorUsername in einem öffentlichen Endpunkt zu dokumentieren, ist das ein guter Moment, um innezuhalten und zu fragen „Braucht der Kunde das wirklich?“.
Wenn Sie die Dokumentation des Path-Parameters hinzufügen möchten (was normalerweise nützlich ist), können Sie das ebenfalls „über“ dem geprüften Szenario tun:
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentSlugPathParam() {
assertThat(mvc.get().uri("/api/public/articles/java-testing-basics"))
.hasStatusOk()
.apply(document("public-article-details-path",
pathParameters(
// Dokumentieren den „Schlüssel“ der Anfrage: was genau in die URL eingesetzt werden muss
parameterWithName("slug").description("Slug des veröffentlichten Artikels")
)
));
}
Aus Sicht des Lesers der Dokumentation beseitigt das eine Menge Fragen: „Ist das eine ID?“, „Ist das ein UUID?“, „Ist das ein menschenlesbarer Pfad?“. Wir sagen explizit: Es ist ein Slug, ein öffentlicher Identifikator.
5. API-Fehler: 404 und 400 für Paginierung
Ein Fehler in der API ist nicht „peinlich, verstecken wir ihn“, sondern genauso ein Vertrag wie 200 OK. Außerdem sind Fehler oft wichtiger: Den Happy Path kann der Kunde „irgendwie verarbeiten“, aber wenn er nicht versteht, was errorCode bedeutet, kann er dem Menschen keine vernünftige Meldung zeigen und schreibt einfach „Oops“.
In ContentHub haben wir ein stabiles ApiProblem-Format (ProblemDetail-kompatibler Stil): Felder wie type, title, status, detail, errorCode, plus manchmal violations. Also können und sollten wir diesen Payload dokumentieren. Eine praktische Strategie ist, separate Snippets für verschiedene Fehlerklassen zu erstellen: nicht gefunden (404), ungültige Paginierungsparameter (400), und optional einen allgemeinen „Problem-Payload-Überblick“.
Wir dokumentieren 404 „Artikel nicht gefunden“. Aus Testsicht ist es wichtig, den Endpunkt mit einem fehlenden slug aufzurufen und den Status 404 zu erhalten. Wie genau der Dienst dazu kommt, ist für den REST Docs Test sekundär; normalerweise wird das durch einen Stub über @MockitoBean gelöst, aber wir dokumentieren nicht den Mock, sondern die Antwort an der HTTP-Grenze.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentArticleNotFound() {
// Szenario: Der Kunde hat einen nicht vorhandenen Artikel angefordert
assertThat(mvc.get().uri("/api/public/articles/missing-slug"))
.hasStatusNotFound() // Vertrag: Für einen fehlenden Artikel geben wir 404 zurück
.apply(document("public-article-not-found",
responseFields(
// Dokumentieren das nützliche Minimum, das der Kunde zur Fehlerbehandlung benötigt
fieldWithPath("title").description("Kurzname des Fehlers"),
fieldWithPath("status").description("HTTP-Status"),
fieldWithPath("detail").description("Beschreibung in Klartext"),
fieldWithPath("errorCode").description("Stabiler Fehlercode")
)
));
}
Beachten Sie: Wir dokumentieren nicht alle Felder von ApiProblem. Das ist wieder bewusste Sparsamkeit. Zum Beispiel können instance und type nützlich sein, aber wenn sie bei Ihnen noch nicht etabliert sind oder manchmal abweichen, sollte man sie nicht zentral machen. Sie können sie später dokumentieren oder als optional markieren. In REST Docs funktioniert fieldWithPath(...).optional() auch für Response-Felder.
Jetzt dokumentieren wir 400 Bad Request für die Paginierung, wenn der Kunde z. B. einen ungültigen size übergeben hat. In einer idealen Welt wäre das ein Validierungsfehler mit violations (z. B. size muss > 0 und <= 100 sein). Und genau das braucht der Kunde: eine Liste der Verstöße.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentInvalidPaginationError() {
// Szenario: Der Kunde hat eine ungültige Seitengröße übergeben
assertThat(mvc.get().uri("/api/public/articles?page=0&size=-1"))
.hasStatusBadRequest() // Vertrag: ungültige Parameter -> 400
.apply(document("public-articles-invalid-pagination",
responseFields(
// Basisfelder des Fehlers
fieldWithPath("status").description("HTTP-Status"),
fieldWithPath("errorCode").description("Stabiler Fehlercode"),
// Validierungsverstöße: Der Kunde kann das Feld hervorheben und die Meldung anzeigen
fieldWithPath("violations[].field").description("Feld mit Verstoß"),
fieldWithPath("violations[].message").description("Beschreibung des Verstoßes")
)
));
}
Wenn violations nicht immer vorhanden ist (z. B. bei 404 ist es nicht da), dann kann man es im „allgemeinen“ Snippet als optional markieren. Aber ich würde nicht sofort ein „universelles Fehler-Snippet für alle Fälle“ machen, weil es zu vage wird. Praktischer ist es, 2–3 konkrete Dokumentationsszenarien zu haben: eins für not-found, eins für Validation, und fertig.
Letztendlich zeichnet sich ein sehr gesunder Gedanke ab: Ein Endpunkt darf ruhig zwei Dokumentations-Snippets haben – Happy Path und Error Path. Das ist für den Kunden oft sogar wichtiger als „noch ein Feld in der erfolgreichen Antwort“.
Um das Bild zu festigen, hier ein Mini-Diagramm, wie das in der Praxis im Kopf (und im Projekt) aussieht:
flowchart TD
A["MVC-Test (MockMvcTester)"] --> B["Assertions: Status, JSON, Header"]
B --> C["document(...): Query-Parameter / Response-Felder"]
C --> D["generierte Snippets"]
D --> E["API-Dokumentation (wird aus Snippets zusammengesetzt)"]
Wichtige Betonung: Die Dokumentation steht auf den Schultern eines geprüften Szenarios. Wir malen den Vertrag nicht von Hand; wir lassen ihn aus einem Test wachsen, der bereits bewiesen hat, dass der Vertrag wirklich so ist.
6. Lebendige REST Docs: Snippets und stabile Namen
Für die öffentliche API von ContentHub ist es praktischer, sich an explizite path-ähnliche Snippet-Namen zu halten: Am Namen erkennt man sofort den Endpunkt und das Szenario. {method-name} bleibt eine funktionierende Alternative, aber hier ist es wichtiger, dass der Baum generated-snippets selbst erzählt, was genau dokumentiert ist.
Wenn REST Docs gerade in das Projekt kommt, ist die erste Emotion oft: „Oh, cool, jetzt haben wir eine Dokumentation!“. Die zweite Emotion eine Woche später: „Oh nein, jetzt haben wir auch noch eine Dokumentation…“. Damit REST Docs nicht zu einer „Steuer auf das Leben“ wird, braucht es zwei Dinge: vernünftig auswählen, was dokumentiert wird, und die Snippets wiederverwendbar machen, aber ohne Magie.
Beginnen wir mit den Snippet-Namen. Das praktischste Schema ist die Verwendung von Namen, die wie ein Pfad lesbar sind: API-Zone + Endpunkt + Szenario. Dann wird build/generated-snippets zu einem fast selbst-dokumentierenden Baum. Zum Beispiel:
| Snippet-Name | Was es ist |
|---|---|
| public-articles-list | public list Happy Path |
| public-article-details | public details Happy Path |
| public-article-not-found | 404 auf details |
| public-articles-invalid-pagination | 400 auf list |
Das ist nicht der einzig richtige Ansatz, aber er hilft, die Snippets nicht zu einer Sammlung von „doc1“, „doc2“, „doc-final-final2“ zu machen.
Jetzt – Wiederverwendung. Die „sicherste“ Variante der Wiederverwendung ist, nicht den gesamten Test in ein DSL zu verstecken, sondern nur die wiederholbaren Teile auszulagern: zum Beispiel die Beschreibung der Basisfelder von ApiProblem. Dann wird die Dokumentation einheitlich, und der Test bleibt lesbar.
import org.springframework.restdocs.payload.FieldDescriptor;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
// Kleiner Helfer: gemeinsame FieldDescriptor an einem Ort halten
final class ApiProblemDoc {
static FieldDescriptor[] basic() {
return new FieldDescriptor[] {
// Minimaler Fehlervertrag, den wir als stabil betrachten
fieldWithPath("title").description("Kurzname des Fehlers"),
fieldWithPath("status").description("HTTP-Status"),
fieldWithPath("detail").description("Fehlerbeschreibung"),
fieldWithPath("errorCode").description("Stabiler Fehlercode")
};
}
}
Dann verwenden Sie das im Test und fügen spezifische Felder des jeweiligen Szenarios hinzu, z. B. violations. Ja, man muss die Deskriptor-Arrays zusammenkleben (manuell oder mit einem kleinen Helfer), aber die Idee ist einfach: Das gemeinsame Minimum lebt an einem Ort, und die „Spezifik“ bleibt im Test.
Ein weiteres Prinzip, das vor „Snippets um der Snippets willen“ bewahrt: Dokumentieren Sie das, was wirklich stabil und nützlich ist. Wenn ein Feld dynamisch berechnet wird und seine Bedeutung für den Kunden nicht relevant ist, machen Sie es nicht zum Zentrum der Dokumentation. REST Docs ist nicht die Pflicht, jedes Byte zu beschreiben, sondern die Pflicht, den Vertrag zu beschreiben.
Und zum Schluss: Haben Sie keine Angst davor, dass ein Endpunkt mehrere Snippets hat. Für Kunden ist das normal: Dieselbe URL hat verschiedene Szenarien und verschiedene Antworten. Eine Dokumentation, die ehrlich „erfolgreiche Antwort“ und „Fehler“ zeigt, ist eine erwachsene Dokumentation. Eine Dokumentation, die so tut, als gäbe es keine Fehler, ist wie eine Bedienungsanleitung für einen Wasserkocher ohne Abschnitt „Was tun, wenn er nicht angeht“.
7. Typische Fehler in Spring REST Docs
Fehler Nr. 1: Dokumentieren, bevor Sie den Vertrag mit Assertions geprüft haben.
Es ist sehr leicht, sich in REST Docs zu verlieren und document(...) als Hauptziel des Tests zu schreiben. Am Ende dokumentieren Sie ein Szenario, das eigentlich gar nicht geprüft wird. Das macht die Dokumentation zu einer schönen Theorie. Das Mittel ist einfach und langweilig: Zuerst werden Status, Header und sinnvolle Antwortfelder mit Assertions geprüft, und erst dann wird document(...) darüber gehängt.
Fehler Nr. 2: Versuchen, in der Dokumentation „das gesamte JSON“ zu beschreiben, einschließlich zufälliger Felder.
Ein Schnappschuss „die ganze Antwort wie sie ist“ sieht verführerisch aus, aber er bricht sehr schnell bei jeder Refaktorisierung und erzeugt Angst, den Vertrag zu ändern, selbst wo es sicher ist. In REST Docs ist es besser, stabile und nützliche Felder zu dokumentieren, anstatt die Dokumentation in ein Museum der Serialisierung zu verwandeln. Wenn Sie partielle Dokumentation wünschen, verwenden Sie den Relaxed-Ansatz oder dokumentieren Sie nur wichtige Teile.
Fehler Nr. 3: Nur den Happy Path dokumentieren und den Fehlervertrag ignorieren.
In der Praxis stolpern Kunden häufiger über Fehler als über erfolgreiche Antworten. Wenn Sie 200 OK dokumentiert, aber ApiProblem für 404 und 400 nicht dokumentiert haben, wird der Kunde die Fehlerbehandlung trotzdem „nach Gefühl“ schreiben. Dann bekommt Ihr Team Fragen: „Wie unterscheidet man ARTICLE_NOT_FOUND von ACCESS_DENIED?“ und „Wo sind die violations?“. Fehler sind Teil der API, und REST Docs ist gerade dadurch nützlich, dass es einen zwingt, das anzuerkennen.
Fehler Nr. 4: Dokumentation der Paginierung und des Listenelements so vermischen, dass niemand etwas versteht.
Wenn Sie in einem Snippet versuchen, sowohl page/size/totalPages als auch content[].slug/title/summary und noch die verschachtelten Kategoriefelder zu beschreiben, wird die Dokumentation unlesbar. Besser dokumentieren Sie den Paginierungs-Wrapper als Seitenstruktur und getrennt – die Elementstruktur (Artikelkarte). Auch wenn das zwei Snippets sind, wird der Leser Ihnen dankbar sein.
Fehler Nr. 5: Instabile Snippet-Namen und „doc-final3“ im Repository.
Anfangs scheint der Name eine Kleinigkeit zu sein. Später haben Sie 40 Snippets und verbringen mehr Zeit mit der Suche nach „wie hieß noch das Snippet für not found“ als mit der eigentlichen Arbeit. Ein stabiles Benennungsschema (public/list/details/error) ist keine Bürokratie, sondern Navigation im Projekt, wenn Tests und Dokumentation zahlreich werden.
GO TO FULL VERSION