CodeGym /Kurse /Spring Test /MockMvc und Live-Server: Ein Szenario

MockMvc und Live-Server: Ein Szenario

Spring Test
Level 21, Lektion 3
Verfügbar

1. Ein Szenario: zwei Beweismodi

Manchmal scheint die Wahl zwischen MockMvc und dem Live-Server-Test einfach eine Frage des Geschmacks zu sein: Dem einen gefällt die Syntax andExpect(...), dem anderen die Fluent-Kette restClient.get(...).uri(...).exchange(). In der Praxis sind es unterschiedliche Beweismodi, nicht unterschiedliche DSLs. Und wenn Sie sich nicht klarmachen, was der Test genau beweist, erhalten Sie schnell eine große, langsame Testsuite, die zweimal dasselbe prüft … und dennoch wichtige Fehler übersieht.

Es ist hilfreich, sich den Test wie einen „Kassenbon im Geschäft“ vorzustellen. Er muss nicht lang sein, er muss ehrlich sein: Was wurde gekauft, zu welchem Preis und was können Sie nun als garantiert betrachten. Ein Full-Context-Test mit MockMvc sagt: „Ich habe die Anfrage durch Spring MVC und den vollständigen Anwendungskontext laufen lassen, aber nicht durch einen echten Server und Netzwerktransport.“ Ein Live-Server-Test sagt: „Ich bin mit einem echten HTTP-Client zu einem echten Server auf einem echten Port gegangen.“ Diese Belege ähneln sich, aber ihre rechtliche Beweiskraft ist unterschiedlich.

Und daraus ergibt sich das Hauptprinzip dieser Lektion: Wir behalten nicht zwei Tests für dasselbe Szenario, nur weil wir es können. Wir behalten einen Live-Server-Test nur dort, wo er mehr Vertrauen schafft, als es der Full-Context-MockMvc kann.

2. Grenze zwischen Mock und Realität

Wenn gesagt wird „MockMvc ist ein Mock“, stellen sich Anfänger manchmal vor, dass alles „gemalt“ und nicht echt sei. Das ist unfair: Im Full-Context-Modus bringt MockMvc sehr viel Reales hoch. Aber „nicht echt“ sind dort genau Server und Transport: Es gibt keinen lauschenden Port, keine echte Verbindung, keinen echten Client, keinen Netzwerk-Roundtrip. Und diesen Unterschied muss man einmal mit eigenen Augen sehen – dann wird man ihn nie wieder verwechseln.

Schematisch lässt sich der Anfragepfad in den beiden Modi so darstellen:

flowchart TD
    T["Testcode"] -->|MockMvc| M["MockMvc (gemockte Servlet-Umgebung)"]
    M --> D["DispatcherServlet + Spring MVC"]
    D --> C["Controller"]
    C --> S["Service"]
    S --> R["Repository"]
    R --> DB["DB"]

    T2["Testcode"] -->|RestTestClient| H["HTTP-Client"]
    H -->|TCP/HTTP| SVR["Embedded-Server (RANDOM_PORT)"]
    SVR --> D2["DispatcherServlet + Spring MVC"]
    D2 --> C2["Controller"]
    C2 --> S2["Service"]
    S2 --> R2["Repository"]
    R2 --> DB2["DB"]

In beiden Zweigen haben wir fast die gleiche „Mitte“: DispatcherServlet, Controller, Services, Repositories, Serialisierung, Fehlerbehandlung – all das funktioniert. Aber links betreten Sie MVC durch die „Seitentür“ (MockMvc), und rechts durch den Haupteingang (echter Server, echter Port, echter HTTP-Client).

Die praktische Schlussfolgerung ist einfach. Wenn Ihr Problem potenziell „in der Mitte“ liegt (Controller-Mapping, Validierung, Advice, Security-Chain, Serialisierung), dann liefert MockMvc bereits einen hervorragenden Beweis. Wenn das Risiko mit dem zusammenhängt, was vor dem Eintritt in MVC geschieht (echter Transport, Container-Eigenschaften, Client-Verhalten), dann ergibt ein Live-Server-Test Sinn.

3. Ein Endpunkt in zwei Stilen

Damit der Vergleich nicht abstrakt bleibt, nehmen wir ein bekanntes und sicheres Szenario aus ContentHub: die öffentliche Karte eines veröffentlichten Artikels nach slug. Es ist gut, weil es keine Authentifizierung erfordert und uns nicht zu früh in das Security-Thema zieht, dabei aber den Unterschied zwischen den Modi zeigt.

Angenommen, wir haben Testdaten, die einen veröffentlichten Artikel mit dem slug existing-article in die Datenbank legen. Wir haben im Kurs bereits vereinbart, dass für Integrationstests ein deterministisches Data-Setup heilig ist, daher verwenden wir @Sql (oder eine andere wiederholbare Methode).

Full-Context-Test mit MockMvc

Nach der Testüberschrift ist es wichtig, das Wichtigste zu sehen: Wir heben den vollständigen Kontext hoch, aber webEnvironment bleibt gemockt (standardmäßig ist @SpringBootTest auf MOCK gesetzt), und wir arbeiten mit MockMvc.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// Full-Context-Test: Hebt den gesamten Spring Boot Kontext hoch und führt die Anfrage durch MockMvc aus.
@SpringBootTest
@AutoConfigureMockMvc // Fordert Spring auf, MockMvc zu erstellen und zu injizieren (ohne echten Port/Server).
class PublicArticleFullContextMockMvcTest {

    @Autowired MockMvc mockMvc; // Injiziert MockMvc aus dem Kontext.

    @Test // normale @Test-Annotation.
    @Sql("/db/testdata/published-articles.sql") // Bereitet die Daten deterministisch in der DB vor dem Test vor.
    void returnsDetailsBySlug() throws Exception {
        mockMvc.perform(get("/api/public/articles/{slug}", "existing-article")) // {slug} wird als Pfadvariable eingesetzt.
                .andExpect(status().isOk()) // Überprüft den HTTP-Status auf MVC-Ebene.
                .andExpect(jsonPath("$.slug").value("existing-article")); // Überprüft den JSON-Vertrag der Antwort.
    }
}

Was genau beweist dieser Test? Er beweist, dass unser vollständiger Kontext tatsächlich zusammengebaut wird, dass das RequestMapping korrekt ist, dass die Serialisierung der Antwort funktioniert und dass der JSON-Vertrag auf der Web-Ebene mit der Erwartung übereinstimmt. Wenn Sie @RequestMapping zerstören, den erforderlichen Controller-Bean entfernen oder den Feldnamen slug im DTO ändern – der Test wird fehlschlagen. Und zwar schnell.

Live-Server-Test mit RestTestClient

Jetzt machen wir fast dasselbe, ändern aber grundlegend den Einstieg: Wir starten den Server auf einem echten Port und gehen mit einem HTTP-Client zu ihm. Hier ist es wichtig, den Port nicht zu „erraten“ und die URL nicht manuell zusammenzubauen, deshalb verwenden wir relative URIs.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class PublicArticleRunningServerTest {

    @Autowired
    RestTestClient restClient;

    @Test
    @Sql("/db/testdata/published-articles.sql")
    void returnsDetailsBySlug() {
        var exchange = restClient.get()
                .uri("/api/public/articles/{slug}", "existing-article")
                .exchange();

        exchange.expectStatus().isOk();
        String body = exchange.expectBody().returnResult().getResponseBodyAsString();
        assertThat(JsonPath.read(body, "$.slug")).isEqualTo("existing-article");
    }
}

Hier haben wir den Code etwas „gestrafft“, um nicht in API-Details zu ertrinken, aber der Kern ist klar. Die Anfrage geht an einen lebenden Server, die Antwort kommt als echte HTTP-Antwort, und den Body überprüfen wir mit gewohnten Werkzeugen (z. B. JsonPath).

Ja, dieser Test kostet mehr Zeit. Aber er gibt eine andere Klasse von Vertrauen: „Über einen echten Port funktioniert das wirklich.“ Das ist nützlich, wenn Sie Probleme vermuten, die in einer gemockten Umgebung nicht sichtbar sind.

4. Stärken des Full-Context-MockMvc

MockMvc im vollständigen Kontext ist keine „Babyversion eines Integrationstests“. Es ist ein sehr mächtiges Werkzeug, das den Großteil der typischen Risiken eines normalen Spring Boot MVC-Dienstes abdeckt. Und das tut es relativ schnell, mit guter Diagnostik und fast ohne infrastrukturellen Kopfschmerz. Wenn Sie Geschwindigkeit beim Feedback lieben (und Sie werden es lieben, wenn die Suite nicht aus 3, sondern aus 300 Tests besteht), dann ist MockMvc Ihr bester Freund.

In erster Linie fängt der Full-Context-MockMvc hervorragend Wiring-Fehler und Probleme an der Web-Grenze ein. Wenn Sie versehentlich @ControllerAdvice entfernen, die Serialisierungseinstellungen ändern, einen Filter hinzufügen, der die Verarbeitung stört, oder einen Endpunkt so „reparieren“, dass er 204 statt 200 zurückgibt – MockMvc gibt Ihnen dafür gerne eine schlechte Note. Besonders angenehm ist, dass der Fehler oft mit einem Stacktrace einhergeht, der direkt auf die Problemstelle im Code verweist – ohne die Zwischenschicht „echter HTTP-Client → Netzwerk → Server“.

Eine weitere Stärke von MockMvc ist die Möglichkeit, Tests dort ausreichend „weiß“ (White-Box) zu halten, wo es sinnvoll ist. Sie können zum Beispiel @Sql verwenden, nach der Anfrage aus dem Repository lesen oder sehen, welche Beans hochgefahren sind. Das ist nicht immer nötig, beschleunigt aber manchmal die Diagnose. Trotzdem durchlaufen Sie echte Controller, echte Services, echte Repositories – es ist also kein Mock-Theater.

Und das Letzte, was wir ehrlich sagen müssen: MockMvc liefert oft eine so gute Abdeckung des Web-Verhaltens, dass Live-Server-Tests zu bloßer Doppelarbeit werden. Daher bleiben in einer gesunden Strategie normalerweise viele Prüfungen bei MockMvc und nur ein kleiner „Kontrollschuss“ geht auf den Live-Server.

5. Was der Live-Server bringt

Wenn wir die Anwendung im Modus RANDOM_PORT starten, ändert sich nicht nur die Geschwindigkeit, sondern die gesamte „Physik“ des Tests. Wir haben einen echten Port, einen echten HTTP-Client und einen echten Container, der die Verbindung annimmt. Das bedeutet, dass Sie endlich all das überprüfen, was an den Rändern des Systems liegt: wie die Anfrage tatsächlich zum Server gelangt, wie der Server die Antwort tatsächlich formt und wie der Client sie tatsächlich wahrnimmt.

In vielen Projekten hilft dies, Problemklassen zu fangen, die sich in einer gemockten Umgebung nur schwer nachbilden lassen. Zum Beispiel Konfigurationsfehler des Embedded-Servers (seltsame Port-/Adresseinstellungen, Startprobleme), Besonderheiten bei der Verarbeitung großer Payloads, Merkwürdigkeiten bezüglich des Kontextpfads oder der Kodierungen oder Situationen, in denen Sie sicherstellen möchten, dass das System als „Blackbox“ funktioniert: nur HTTP-Eingang und HTTP-Ausgang, ohne Einblicke in die Mitte.

Außerdem zwingt der Live-Server-Modus Sie dazu, ehrlicher mit den Daten umzugehen. In MockMvc ist es sehr leicht, den Test unbeabsichtigt in ein „internes Szenario“ zu verwandeln: Sie speichern die Entität über das Repository, machen sofort eine Anfrage, prüfen sofort – und alles sieht hübsch aus. Auf einem Live-Server müssen Sie sich daran erinnern, dass Client und Server getrennt sind, daher ähnelt der Test eher den realen Betriebsbedingungen.

Aber Ehrlichkeit hat ihren Preis. Live-Server-Tests sind in der Regel langsamer, erfordern ein sorgfältigeres Data-Setup und leiden häufiger unter „schmutzigem Zustand“, wenn Sie nicht vorsehen, wie die Datenbank in den Ausgangszustand zurückversetzt wird. Deshalb sprechen wir von einer kleinen Teilmenge: Der Live-Server wird dort benötigt, wo er das Vertrauen stärkt, und nicht dort, wo er einfach die Ausführungszeit verdoppelt.

6. Transaktionen und Data-Setup auf dem Live-Server

Hier hört der Unterschied zwischen den beiden Modi auf, Theorie zu sein. Nach dem Full-Context-MockMvc gewöhnt man sich leicht an das Gefühl, dass @Transactional im Test fast automatisch für einen sauberen Zustand sorgt. Im Running-Server-Pfad bricht diese Intuition zusammen.

In MockMvc verläuft die Anfrage innerhalb des Prozesses, daher erscheint die Transaktionsmagie manchmal näher, als sie tatsächlich ist. Im RANDOM_PORT nimmt der Server die Anfrage in einem anderen Thread entgegen und öffnet seine eigenen Transaktionen. Der Test und die Verarbeitung des HTTP-Aufrufs leben nicht mehr in derselben „Tasche“.

Das hat zwei praktische Konsequenzen. Erstens: Nicht festgeschriebene Daten, die der Test innerhalb seiner eigenen Transaktion vorbereitet hat, kann der Server möglicherweise einfach nicht sehen. Zweitens: Änderungen, die der Server bei der Verarbeitung der Anfrage selbst festgeschrieben hat, müssen nicht zusammen mit dem Test zurückgerollt werden.

Daher ist es für Live-Server-Szenarien besser, Data-Setup und Cleanup explizit zu halten: deterministisches @Sql, wiederholbare Seed-Skripte und separate Bereinigung, wo der Test den Zustand ändert. Derselbe Controller-Flow kann mit beiden Methoden getestet werden, aber die Transaktionsdisziplin ist jeweils unterschiedlich.

7. Auswahl des Testmodus

Wenn Sie den Live-Server-Modus in der Hand haben, besteht die Versuchung, alles darauf umzustellen. Das ist etwa so, als würde man ein neues Messer kaufen und damit Brot, Tomaten, Steak, Verpackungen, Kabel und gleich noch die moralischen Grundsätze schneiden. Das Messer ist gut. Aber die Teststrategie geht nicht um das „coolste Werkzeug“, sondern um das richtige Werkzeug für das Risiko.

Es ist praktisch, eine einfache Tabelle im Kopf zu haben (nicht als Gesetz, sondern als Orientierungshilfe):

Risiko / Frage Häufig ausreichend: @SpringBootTest + @AutoConfigureMockMvc Manchmal erforderlich: RANDOM_PORT + RestTestClient
Endpoint-Mapping, @RequestMapping, DTO-Serialisierung, @ControllerAdvice, Validation Ja Selten
Security-Filter-Chain, Filter, allgemeine Spring-Web-Infrastruktur Ja Manchmal (wenn Sie einen echten Edge-Effekt vermuten)
Probleme beim Start des echten Servers, Port-Binding, echter HTTP-Roundtrip Nein Ja
Kein Wunsch, in die Anwendung zu „lugen“, und Wunsch nach einer Blackbox Manchmal Ja
Suite-Geschwindigkeit und schnelles Feedback Ja Nein (teurer)
Deterministisches Rollback und einfache Isolation Meist einfacher Muss separat diszipliniert werden

Im Rahmen von ContentHub sieht ein gesundes Bild normalerweise so aus: Die meisten Integrationstests bleiben auf MockMvc (weil sie schnell sind und Regressionen an der Web-Grenze hervorragend abfangen), und die Live-Server-Tests sind eine kleine Menge von „Kontroll“-Szenarien, die beweisen, dass die Anwendung als Dienst tatsächlich hochkommt und per HTTP so antwortet, wie der Client es erwartet.

Das heißt: Zuerst antworten wir auf die Frage, ob ein echter Server nötig ist, und erst dann wählen wir das Client-DSL innerhalb dieses Modus.

8. Typische Fehler beim Vergleich von MockMvc und Live-Server-Tests

Fehler Nr. 1: Zu glauben, dass beide Tests „Integrationstests“ sind und daher austauschbar.
Das ist eine Formulierungsfalle. Beide Tests starten zwar den vollständigen ApplicationContext, aber sie beweisen unterschiedliche Dinge. MockMvc beweist die Arbeit der Spring-MVC-Kette und das Wiring innerhalb der Anwendung; der Live-Server-Test beweist die Arbeit der Anwendung als Netzwerkdienst. Wenn Sie diese Beweise nicht unterscheiden, beginnt die Testsuite nach dem Prinzip zu leben „Hauptsache viele Tests“, statt „Hauptsache viel Vertrauen“.

Fehler Nr. 2: Dasselbe Szenario in beiden Modi ohne zusätzlichen Mehrwert zu duplizieren.
Manchmal macht man das „zur Sicherheit“, und das führt schnell dazu, dass die Suite doppelt so langsam wird und das Team die Tests lokal deaktiviert (weil Warten niemand mag). Fragen Sie sich lieber ehrlich: Was fügt der Live-Server in diesem Szenario hinzu? Wenn die Antwort „nichts, nur eine andere Syntax“ lautet, dann fügt er nichts hinzu.

Fehler Nr. 3: Sich auf @Transactional als universellen Bereinigungsmechanismus im Live-Server-Modus zu verlassen.
Dieser Fehler ist besonders tückisch, weil sich die Transaktionalität in MockMvc (und im Data-Slice) tatsächlich oft als „bequeme Magie“ verhält. Aber im RANDOM_PORT bekommen Sie getrennte Threads und getrennte Transaktionen: Server-Änderungen müssen nicht zusammen mit dem Test zurückgerollt werden. Wenn Sie das vergessen, bekommen Sie mit ziemlicher Sicherheit „schmutzige“ Daten und instabile Tests.

Fehler Nr. 4: Live-Server-Anfragen über absolute URLs und @LocalServerPort ohne Notwendigkeit zu bauen.
Manchmal erscheint es logisch, http://localhost: + Port + ... zu schreiben, aber in der Realität wird das schnell zu Lärm: Der Port wird Teil des Testcodes, die URL wird „von Hand zusammengebaut“, und die Lesbarkeit des Szenarios sinkt. Relative URLs sind genau der Kompromiss, der den Live-Server erhält, ohne den Test zur Adresszusammenstellung zu machen.

Fehler Nr. 5: Die Modi nach der „Schönheit der API“ vergleichen, nicht nach dem Anfrageverarbeitungspfad.
Es ist sehr leicht, sich davon mitreißen zu lassen, dass das Chaining von RestTestClient modern aussieht und MockMvc „alt“ wirkt. Aber in der realen Arbeit besteht Ihre Aufgabe nicht darin, ein modisches DSL zu wählen, sondern den minimal ausreichenden Beweismodus. Daher sollte der Vergleich immer mit der Frage beginnen: „Wo verläuft die Anfrage? Gibt es einen echten Port? Gibt es einen echten Client?“

1
Aufgabe
Spring Test, Level 21, Lektion 3
Gesperrt
Ein Happy Path durch MockMvc und Live-Server
Ein Happy Path durch MockMvc und Live-Server
1
Aufgabe
Spring Test, Level 21, Lektion 3
Gesperrt
Ein negative path über MockMvc und Live-Server
Ein negative path über MockMvc und Live-Server
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION