1. Szenario create draft → submit als Regression Candidate
Wenn man das Backend mit den Augen eines unerfahrenen Entwicklers betrachtet, scheint es: „Was ist da schon schwer – zwei Endpunkte, zwei HTTP-Anfragen.“ Aber gerade an solchen „einfachen“ Stellen entstehen am häufigsten Regressionen, weil sie gleich mehrere Schichten betreffen. In ContentHub ist das Szenario des Erstellens eines Entwurfs und des Einreichens zur Überprüfung die Eingangstür zum gesamten Publication Workflow: Wenn diese Tür klemmt, kann man nichts mehr testen, weil der Geschäftsprozess nicht startet.
Es ist wichtig, schnell die Begriffe zu klären, damit wir nicht aneinander vorbeireden – einer über „Annotation“, der andere über „Schmerz in der Produktion“.
| Begriff | Einfache Erklärung | In unserem Szenario |
|---|---|---|
| Regression Suite | Eine kleine Sammlung teurer End-to-End-Tests, die regelmäßig ausgeführt werden, um Fehler im Kernverhalten zu erkennen | Einige Tests für die Hauptabläufe: Submit, Publish, Reject, Archive |
| Regression Candidate | Ein Szenario, das einen Platz in der Regression Suite beansprucht, weil sein Ausfall auffällig und teuer ist | create draft → submit – bricht schnell und schmerzhaft |
| Regression Checkpoint | Eine Zwischenprüfung innerhalb eines langen Szenarios, um den Test diagnostizierbar zu machen | Nach Create: id vorhanden, Status DRAFT; nach Submit: Status IN_REVIEW |
| Beobachtbares Ergebnis | Das, was wir von außen prüfen können: HTTP-Antwort und/oder Datenzustand | Status, Zeitstempel, Tatsache der Speicherung in der DB |
Und nun der springende Punkt: Unser Regression-Test sollte nicht zu einem Museum aller möglichen Prüfungen werden. Die vertraglichen Details der DTOs haben wir bereits über @JsonTest abgedeckt, die HTTP-Mechanik und Validierung über @WebMvcTest. Im Regression-Test behalten wir nur das, was das Signal „Der durchgängige Ablauf lebt“ liefert, ohne den Test in ein zerbrechliches Laken zu verwandeln.
2. Grenzen des Szenarios und Checkpoint-Denken
Ein langer Integrationstest hat ein typisches Problem: Er schlägt fehl, und man sieht rot … aber man versteht nicht, wo genau das Unglück passiert ist. Das ist wie in einem Café: Man bestellt Kaffee, Dessert und Suppe, und bekommt „irgendwas“. Die Frage: Was genau? War der Kaffee kalt? Das Dessert salzig? Die Suppe war eigentlich Tee? Ohne Checkpoints rät man nur.
Deshalb formulieren wir die Grenzen des Szenarios explizit. Start: Der Artikel existiert nicht. Finale: Der Artikel existiert und befindet sich im Status IN_REVIEW (d.h. der Moderationszweig OK/WARN wurde erfolgreich durchlaufen). Dazwischen setzen wir einige Kontrollpunkte, die über beobachtbare Ergebnisse geprüft werden.
Kleine Übersicht des Szenarios:
flowchart TD
A["POST /api/editor/articles create draft"] --> B["Checkpoint 1 201 Created, id != null, status=DRAFT"]
B --> C["POST /api/editor/articles/{id}/submit submit for review"]
C --> D["Checkpoint 2 status=IN_REVIEW"]
D --> E["Final proof DB read: Article.status=IN_REVIEW"]
Beachten Sie einen wichtigen Punkt: Checkpoints bedeuten nicht „Lasst uns alles Mögliche prüfen“. Es sind Punkte, die helfen, einen Fehlschlag zu diagnostizieren. Wenn es am ersten Checkpoint scheitert, liegt das Problem bei der Erstellung. Wenn der erste bestanden wird und der zweite fehlschlägt, liegt es an der Submit-Mechanik, der Moderation oder dem Statusübergang.
3. Testmodus: @SpringBootTest + @AutoConfigureMockMvc
Wenn man zwei Modi kennt – Full-Context MockMvc und Live-Server RANDOM_PORT – könnte die Hand zum „ehrlichsten“ greifen: einen echten Port hochziehen und HTTP „wie im echten Leben“ laufen lassen. Aber eine Regression Suite ist immer ein Kompromiss zwischen Aussagekraft und Kosten. Wenn jeder Test „wie im echten Leben“ ist, hat man schnell eine Suite, die wie eine Serie läuft: morgens gestartet, mittags fertig, und dann geht man Kaffee holen, weil der Fortschrittsbalken wehtut.
Für das Szenario create draft → submit for review bringt Full-Context + MockMvc oft einen sehr guten ROI. Wir prüfen tatsächlich das Wiring der Schichten (Controller → Service → Repository → Integrationsadapter), Serialisierung, Validierung, Error Handling und viele andere Dinge, bezahlen aber nicht für den echten TCP/HTTP-Roundtrip.
Und noch ein wichtiges Detail: In Regression-Szenarien brauchen wir fast immer Zugriff auf die Datenbank für den endgültigen Nachweis. Nicht um „das Repository zu testen“ (das haben wir bereits mit @DataJpaTest gemacht), sondern um den endgültigen Zustand des Geschäftsobjekts zu bestätigen. Daher kombinieren wir HTTP-Aufrufe und eine abschließende Prüfung über ArticleRepository.
4. Gerüst der Testklasse
Wenn man einen Regression-Test schreibt, möchte man mit dem „Leckersten“ beginnen: sofort mvc.perform(...) schreiben. Aber es ist besser, zuerst das Gerüst zu erstellen und sich darauf zu einigen, welchen Verhaltenszweig wir fixieren. Im Submit-Flow gibt es mehrere Ausgänge (z.B. OK/WARN führt zu IN_REVIEW, BLOCK zu REJECTED). In einem einzelnen Regression-Test wählen wir einen Zweig, sonst wird der Test zu einer „Allzweckmaschine“ mit mehreren Ausgängen und ist nicht mehr lesbar.
Das grundlegende Regression-Skelett ist hier dasselbe wie bei anderen Business-Flows in der Suite: @SpringBootTest + @AutoConfigureMockMvc + @ActiveProfiles("test"). Die lokalen properties unten ersetzen nicht das test-Profil, sondern legen darüber nur den gewünschten Moderationszweig fest.
Nachfolgend das minimale Gerüst. Beachten Sie properties: Dies ist eine einfache Möglichkeit, den Moderationszweig im Test zu fixieren. Der genaue Property-Name hängt von Ihrem Projekt ab, aber der Sinn ist: Der Test sollte externe Entscheidungen steuern, sonst ist er von der „zufälligen Stimmung“ des Stubs abhängig.
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootTest(properties = "contenthub.moderation.test-decision=OK") // Lokaler Moderation-Pfad, zusätzlich zum Test-Profil
@AutoConfigureMockMvc // Spring-Kontext vollständig hochfahren und MockMvc ohne echten Port konfigurieren
@ActiveProfiles("test") // Allgemeine Testumgebung für die Regression Suite
class SubmitForReviewRegressionTest {
@Autowired
MockMvc mvc; // Hauptwerkzeug: HTTP-Anfragen an Controller senden und Antworten prüfen
}
Falls Sie bereits ein ArticleRepository haben, fügen wir es später hinzu – es wird für die abschließende Zustandsprüfung benötigt. Ja, technisch könnte man das Finale auch über GET /api/editor/articles/{id} prüfen, aber das Lesen aus dem Repository ist meist kürzer und schneller, und uns geht es um das Signal des gespeicherten Zustands.
5. Checkpoint 1: create draft und id
Das Erstellen eines Entwurfs ist der erste Schritt im Workflow, und genau hier beginnen Tests oft „Haare zu bekommen“: Manche prüfen alle Felder der Antwort, manche versuchen, die halbe Domäne hineinzupacken, manche testen versehentlich den JSON-Vertrag erneut. Wir machen es anders: Wir senden die minimal gültige Anfrage, prüfen nur die wichtigsten Erfolgsmerkmale, extrahieren die id und machen weiter.
Zuerst fügen wir das Repository und die nötigen Imports für die Asserts hinzu. Die Imports sollten ordentlich bleiben – Regression muss nicht wie eine Enzyklopädie aussehen.
import org.junit.jupiter.api.Test; // Test-Annotation (im Projekt JUnit 6 verwenden)
import org.springframework.http.MediaType;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@Test
void createDraft_thenSubmit_movesArticleToInReview() throws Exception {
// Arrange/Act/Assert fügen wir unten hinzu: momentan nur das Szenariogerüst
}
Jetzt brauchen wir einen Helfer createDraft(...), der die HTTP-Anfrage stellt, die grundlegenden Erfolgsmerkmale prüft und die id zurückgibt. Wichtiger Punkt: Der Helfer ist hier akzeptabel, weil er das Transportrauschen (JSON-Parsing) entfernt, aber nicht verbirgt, was genau wir tun.
private Long createDraft(String title) throws Exception {
// Minimal gültiges JSON vorbereiten: nur das Nötigste für die Entwurfserstellung
String json = """
{"title":"%s","summary":"Short summary","body":"Text","category":"java"}
""".formatted(title);
// POST create draft ausführen und Checkpoint 1 festhalten: HTTP 201 + status=DRAFT
String body = mvc.perform(post("/api/editor/articles")
.contentType(MediaType.APPLICATION_JSON) // Dem Controller mitteilen, dass es JSON ist
.content(json)) // Request-Body
.andExpect(status().isCreated()) // Checkpoint: Entwurf wurde tatsächlich erstellt
.andExpect(jsonPath("$.status").value("DRAFT")) // Checkpoint: Geschäftsstatus im ersten Schritt
.andReturn().getResponse().getContentAsString(); // Antwort-Body holen, um die id zu extrahieren
// ID des erstellten Artikels zurückgeben: wird für den Submit-Schritt benötigt
return ((Number) com.jayway.jsonpath.JsonPath.read(body, "$.id")).longValue();
}
Und noch eine Grenze des Umfangs. In diesem Szenario weisen wir den Business-Flow selbst und seine Auswirkungen auf die Daten nach: Der Entwurf wurde erstellt und dann in IN_REVIEW überführt. Wir gehen davon aus, dass die Anfrage von einem berechtigten Editor kommt; die Matrix von 401/403, Rollen und Owner-basierten Zugriff schleppen wir nicht in jeden einzelnen Regression-Test, sonst wird der Kern der Suite schnell aufgebläht.
Nun ein paar Bemerkungen, die viel Nerven sparen. Wenn in Ihrem Projekt Editor-Endpunkte geschützt sind und Sie 401 erhalten, ist das kein Grund zur Panik und zum Verdacht auf einen „kaputten Controller“. Meist bedeutet es, dass Sie die Authentifizierung nicht mitgesendet haben. Die einfachste Lösung: Fügen Sie der Anfrage Basic Auth oder einen Test-User-Postprozessor hinzu. Z.B. in die mvc.perform(...) Zeile können Sie .with(httpBasic("editor", "editor-pass")) einfügen, wenn Sie HTTP Basic verwenden. Wenn die Sicherheit im Testprofil deaktiviert ist, ist das nicht nötig – das Prinzip bleibt: Der Regression-Test sollte sich klar als der Benutzer ausweisen, der die Aktion tatsächlich ausführen darf.
Warum prüfen wir genau status=DRAFT und nicht alles andere? Weil „Entwurf erstellt“ ein Geschäftsergebnis ist, das den weiteren Workflow beeinflusst. Im Regression-Test alle Felder der Antwort zu prüfen, wäre eine Wiederholung der JSON-Schicht und des Web-Slices, für die wir bereits früher bezahlt haben.
6. Checkpoint 2: submit und Status IN_REVIEW
Jetzt wird es interessant: Der zweite Schritt des Szenarios muss tatsächlich mehrere Schichten durchlaufen und den Zustand des Artikels ändern. Genau hier treten oft Regressionen durch „kleine Refactorings“ auf: Jemand hat eine Policy geändert, jemand ein Enum umbenannt, jemand vergessen, submittedAt zu speichern, oder jemand hat „ein bisschen“ die Integration mit der Moderation angepasst.
Wir erstellen den Helfer submitForReview(id) und dann die abschließende Prüfung über das Repository. Der Submit selbst kann den aktualisierten Artikel oder einfach 200 OK zurückgeben – in beiden Fällen sind zwei Beweise wichtig: eine erfolgreiche HTTP-Antwort und der gespeicherte Zustand in der DB.
private void submitForReview(Long id) throws Exception {
// Zweiter Schritt des Szenarios: den bereits erstellten Artikel zur Überprüfung einreichen
mvc.perform(post("/api/editor/articles/{id}/submit", id))
.andExpect(status().isOk()); // Checkpoint: Befehl wurde angenommen und ohne Fehler verarbeitet
}
Jetzt binden wir das Repository ein und prüfen das Ergebnis. Hier fügen wir AssertJ hinzu, weil es lesbarer ist als eine Reihe von assertEquals, und wir haben es als grundlegenden Stil des Kurses vereinbart.
import org.springframework.beans.factory.annotation.Autowired;
import static org.assertj.core.api.Assertions.assertThat;
@Autowired
ArticleRepository articleRepository; // Endgültiger Nachweis erfolgt durch Lesen aus der DB
@Test
void createDraft_thenSubmit_movesArticleToInReview() throws Exception {
// Act: Entwurf erstellen
Long id = createDraft("Intro to Spring");
// Act: Zur Überprüfung einreichen
submitForReview(id);
// Assert: Zustand aus der DB erneut lesen und den endgültigen Geschäftsstatus prüfen
Article article = articleRepository.findById(id).orElseThrow();
assertThat(article.getStatus()).isEqualTo(ArticleStatus.IN_REVIEW);
}
Wenn Sie den Nachweis verstärken möchten (ohne in „Alles prüfen“ zu verfallen), können Sie prüfen, ob submittedAt gesetzt wurde. Wir vergleichen nicht die genaue Uhrzeit, da dies oft mehr Probleme als Nutzen bringt, wenn Sie nicht Clock im Testprofil fixiert haben. Aber „nicht null“ zu prüfen, ist in der Regel ein hervorragender Checkpoint.
assertThat(article.getSubmittedAt()).isNotNull(); // Zusätzlicher Checkpoint: Tatsache des Submits in Daten festgehalten
Am Ende erhalten wir ein gutes durchgängiges Signal: Das System hat den Befehl zum Erstellen eines Entwurfs angenommen, eine ID zurückgegeben, dann den Befehl zum Einreichen zur Überprüfung angenommen und den Artikel tatsächlich in den Status IN_REVIEW überführt. Das ist es, was ein Regression-Test nachweisen soll: nicht „wir haben eine Methode aufgerufen“, sondern „das System hat den Weg durchlaufen“.
7. Lesbarkeit des Regression-Tests
Wenn ein Test „durchgängig“ wird, entsteht die Versuchung, ein kleines Framework darum herum zu bauen. Heute machen Sie createDraft(...), morgen submit(...), übermorgen approve(...), und dann wird daraus ein DSL givenDraft().whenSubmit().thenStatusIs(...), und plötzlich haben Sie einen zweiten Spring in den Tests. Das ist fast immer schlecht für das Lernen und oft auch schlecht für ein echtes Projekt, weil ein Testfehler für einen Neuling schwer zu erklären ist.
Ein gesunder Kompromiss sieht so aus: Helfer sollen Routine und Wiederholungen vermeiden, aber die Schritte des Szenarios sichtbar lassen. Die Testmethode sollte wie eine kleine Geschichte lesbar sein: „erstellt → eingereicht → geprüft“. Die Namen der Helfer sollten domänenspezifisch sein, nicht technisch. createDraft klingt wie eine Geschäftsoperation. postToEditorControllerAndParseJsonId klingt wie eine Strafe.
Manchmal ist es nützlich, innerhalb des Tests die drei Teile AAA (Arrange–Act–Assert) zu trennen, aber ohne Fanatismus. In einem Regression-Test ist „Arrange“ normalerweise der Ausgangszustand (bei uns fast leer), „Act“ sind zwei HTTP-Anfragen, „Assert“ ist die abschließende Statusprüfung. Wenn Sie das offensichtlich machen, wird der Test angenehmer zu warten sein, und Sie werden leichter verstehen, was kaputt ist, wenn er rot wird.
8. Typische Fehler im Regression-Test
Fehler Nr. 1: Einen Mega-Test für „alles“ machen.
Manchmal versucht ein Student (oder schlimmer, ein Entwickler) in eine Methode alles zu packen: Erstellen, Bearbeiten, Submit, Approve und sogar die Prüfung der öffentlichen API. Das Ergebnis ist ein langer, zerbrechlicher und schwer diagnostizierbarer Test: Er kann überall fehlschlagen, und Sie verbringen Zeit nicht mit der Fehlersuche, sondern mit Archäologie. In der Regression Suite ist es besser, einen Test – einen Hauptablauf mit klarem Finale – zu haben.
Fehler Nr. 2: Alle Felder der JSON-Antwort prüfen.
Das sieht nach Sorgfalt aus, aber meist dupliziert es günstigere Teststufen. Den JSON-Vertrag des DTOs haben Sie bereits mit @JsonTest stabilisiert, die Web-Details im MVC-Slice. Der Regression-Test braucht nur wenige Felder, die den Sinn belegen: id, status, ggf. slug oder submittedAt. Je weniger Rauschen, desto höher der Signalwert.
Fehler Nr. 3: Sich nur auf HTTP-Status zu verlassen und den Zustand nicht erneut zu lesen.
200 OK nach einem Submit garantiert nicht, dass der Artikel tatsächlich in den richtigen Status übergegangen ist. Irgendwo könnte ein Speicherfehler „geschluckt“ worden sein, ein Teil des Workflows nicht ausgeführt worden sein oder sich die Transaktionsgrenzen geändert haben. Die abschließende Prüfung durch erneutes Lesen (Repository oder separater GET) verwandelt den Test von „wir haben einen Knopf gedrückt“ in „das System hat den Zustand geändert“.
Fehler Nr. 4: Verschiedene Moderationszweige in einem Test vermischen.
Submit-for-Review hat in ContentHub alternative Ausgänge. Wenn Sie in ein und demselben Test sowohl OK als auch BLOCK abdecken wollen, wird der Test zu zwei Szenarien gleichzeitig, und ein Fehlschlag wird weniger offensichtlich. Halten Sie einen Zweig – einen Test. Für einen anderen Zweig gibt es einen anderen Test mit anderer Vorbereitung und anderen Erwartungen.
Fehler Nr. 5: Wichtige Voraussetzungen in zu cleveren Helfer-Methoden verstecken.
Ein Helfer ist gut, solange er Lärm reduziert. Wenn der Helfer aber „im Inneren“ Kategorien erstellt, Benutzer vorbereitet, Properties ändert und dann „irgendwie“ einen Submit macht, wird der Test zu einem nicht lesbaren Szenario. In der Regression Suite ist etwas mehr Klarheit besser als etwas mehr Magie. Ja, das ist der seltene Fall, in dem „klar“ wichtiger ist als „schön“.
GO TO FULL VERSION