1. Parameter webEnvironment in @SpringBootTest
Wenn Sie das Wort web in einem Test sehen, malt das Gehirn automatisch ein Bild: „Aha, dann wird jetzt definitiv der Server gestartet, irgendwo gibt es einen Port, man kann den Browser öffnen und localhost:8080 eingeben“. Spring Boot in Tests liebt es, diese Gewohnheit im guten Sinne zu durchbrechen: Die Web-Infrastruktur kann teilweise hochgefahren, simuliert oder „richtig“ hochgefahren werden – und davon hängen stark die Kosten des Tests und das ab, was er überhaupt beweisen kann.
webEnvironment ist ein Parameter der Annotation @SpringBootTest, der den Modus der Web-Umgebung für den Full-Context-Test festlegt. Es ist wichtig zu verstehen: Es geht nicht darum, „wie viel Spring hochgefahren wurde“ – in @SpringBootTest ist er ohnehin vollständig –, sondern darum, wie sich der Web-Teil der Anwendung verhält: Gibt es einen echten eingebetteten Server, gibt es einen Port, gibt es eine Servlet-Umgebung oder ist das alles nicht nötig.
Es ist nützlich, diesen Unterschied vorab zu klären: Andernfalls erwartet man sehr leicht von einem Full-Context-Test einen echten Server, wo es gar keinen gibt.
Im Klartext gesagt, hat @SpringBootTest vier „Posen“, und webEnvironment wählt die Pose aus. Die Anwendung ContentHub bleibt dabei dieselbe: Controller, Services, Repositories, Security-Konfiguration und Einstellungen werden ohnehin als echte Beans hochgefahren, nur die Web-Umgebung kann ausgeschaltet, simuliert oder real gestartet werden.
2. Vier Modi: NONE, MOCK, RANDOM_PORT, DEFINED_PORT
Wenn Sie jemals an einer Waschmaschine den Waschgang ausgewählt haben, sind Sie bereits bereit für webEnvironment: „ohne Schleudern“, „schnell“, „intensiv“ und „warum piept es wieder und startet nicht“. Aber in Spring Boot ist es etwas rationaler: Die Modi unterscheiden sich danach, ob der Server startet und ob ein echter Netzwerk-Transport vorhanden ist. Um das Bild im Kopf zu behalten, ist es hilfreich, es einmal als Tabelle und als Diagramm zu sehen.
Hier ist eine kompakte Karte des Terrains:
| Modus webEnvironment | Echter Web-Server | Echter Port vorhanden | Web-Infrastruktur (Servlet/MVC-Umgebung) | Typische Rolle in ContentHub |
|---|---|---|---|---|
| NONE | nein | nein | nein | Full-Context-Tests, bei denen HTTP gar nicht benötigt wird: Verkabelung von Services/Repositories, Non-Web-Smoke |
| MOCK (standardmäßig) | nein | nein | ja (Mock-Servlet-Umgebung) | Überprüfung des Verhaltens des Web-Teils ohne Netzwerk: Filter, Konvertierung, Fehlerbehandlung, aber ohne echten Port |
| RANDOM_PORT | ja | ja (zufällig) | ja | Der „echteste“ Modus ohne Port-Konflikte; nützlich, wenn es auf den echten Transport/Runtime ankommt |
| DEFINED_PORT | ja | ja (festgelegt) | ja | Selten nötig; nur wenn ein fester Port zwingend erforderlich ist |
Und hier dieselbe Idee als Diagramm:
flowchart TD
A["@SpringBootTest"] --> B{webEnvironment}
B -->|NONE| C["Kontext ohne Web-Umgebung, kein Server, kein Port"]
B -->|MOCK| D["Mock-Web-Umgebung, MVC-Infrastruktur vorhanden, aber kein Server"]
B -->|RANDOM_PORT| E["Echter eingebetteter Server, Port wird zufällig gewählt"]
B -->|DEFINED_PORT| F["Echter eingebetteter Server, Port festgelegt"]
Der wichtigste praktische Punkt: In MVC-Anwendungen wie unserem ContentHub gilt: Wenn Sie einfach @SpringBootTest ohne Parameter schreiben, erhalten Sie MOCK. Dies ist eine der häufigsten Verwirrungsquellen: Der Test heißt Integration, der Kontext ist vollständig, aber der Server wurde nicht gestartet. Der Port ist nicht belegt, es gibt kein Netzwerk, dafür gibt es Web-Infrastruktur in simulierter Form.
3. WebEnvironment.NONE – vollständiger Kontext ohne Web-Umgebung
Der Modus NONE wirkt oft langweilig, weil das Wort web aus dem Test zu verschwinden scheint. Aber genau darin liegt der Nutzen: Manchmal benötigen Sie einen vollständigen Spring-Kontext, um die Verkabelung, Konfiguration, das Zusammenspiel echter Beans, die Arbeit von Repositories und Services gemeinsam zu überprüfen – aber Sie brauchen die HTTP-Welt überhaupt nicht. In einer solchen Situation spart NONE Zeit und reduziert die Anzahl Fallstricke.
In WebEnvironment.NONE fährt Spring Boot den ApplicationContext als Nicht-Web-Anwendung hoch. Es wird keinen eingebetteten Server geben, auch keinen Port, und die Web-Infrastruktur (Servlet-Kontext, DispatcherServlet und alles drum herum) wird nicht hochgefahren. Dennoch ist es ein Full-Context-Test: Ihre @Service, @Repository, @ConfigurationProperties, Security-Beans, soweit sie nicht an Web gebunden sind – all das wird ordentlich hochgefahren.
Das einfachste Beispiel für ContentHub sieht so aus:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) // Modus explizit wählen: ohne Web-Umgebung
class ContentHubNoneEnvironmentTest {
@Test // einfach prüfen, dass der Kontext startet
void contextStartsWithoutWeb() {
// Wenn der Kontext nicht hochfährt, fällt der Test bereits beim Start durch
}
}
Wenn Sie den Unterschied zu Web-Modi selbst ausprobieren möchten, können Sie eine kleine Prüfung auf das Vorhandensein von WebApplicationContext machen. In NONE wird es normalerweise einfach nicht vorhanden sein:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) // Modus ohne Web-Infrastruktur
class ContentHubNoneHasNoWebContextTest {
// In NONE wird der Web-Kontext normalerweise nicht erstellt, daher bitten wir Spring, 'nicht zu meckern' und null zu liefern
@Autowired(required = false)
WebApplicationContext webContext;
@Test
void webContextIsAbsent() {
// Zeigt den entscheidenden Unterschied: Web-Kontext ist nicht vorhanden
Assertions.assertThat(webContext).isNull();
}
}
Beachten Sie required = false: Es bedeutet so viel wie „meckere nicht, wenn der Bean nicht existiert, gib einfach null zurück“. In der Produktion machen wir das fast nie, aber in Tests ist es manchmal praktisch für Demonstration und Diagnose.
In ContentHub ist dieser Modus gut geeignet, wenn Sie die Kette „Service → Repository → Datenbank“ testen oder einfach sicherstellen möchten, dass die Anwendung im Testprofil zusammenbaut, aber Sie nicht das Verhalten der HTTP-Grenze testen wollen.
4. WebEnvironment.MOCK – Web-Anwendung ohne echten Port
MOCK ist der tückischste Modus, weil der Name klingt wie „naja, das ist nicht richtig echt“, aber in der Praxis ist es dennoch ein vollständiger Kontext, nur ohne echten Server. Er ist wichtig als Zwischenoption zwischen NONE und einem echten Server: Die Web-Infrastruktur wird hochgefahren, Spring MVC als Komponenten existiert, aber alles läuft im Speicher ohne Netzwerktransport. Wenn NONE eine Probe ohne Bühne ist, dann ist MOCK eine Bühne mit Kulissen, aber der Zuschauerraum ist geschlossen.
Ein wichtiges Detail: @SpringBootTest ohne Parameter bedeutet in einer Servlet-Anwendung fast immer MOCK. Das heißt, Folgendes:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest // Für eine Servlet-Anwendung bedeutet das normalerweise WebEnvironment.MOCK
class ContentHubDefaultSpringBootTest {
@Test
void defaultsToMockWebEnvironment() {
// Der Kontext wird vollständig hochgefahren, aber es gibt keinen echten Server und keinen Port
}
}
ist sinngemäß äquivalent zu:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // MOCK-Modus explizit festlegen
class ContentHubExplicitMockEnvironmentTest {
@Test
void startsWithMockWebEnvironment() {
// Praktisch, wenn Sie nicht 'raten' wollen, welchen Modus Spring standardmäßig wählt
}
}
Wie erkennt man, dass kein Server da ist? Der einfachste nicht-magische Weg ist zu prüfen, ob die Eigenschaft local.server.port fehlt. Sie taucht auf, wenn ein echter Server in Server-Modi startet. In MOCK ist sie normalerweise nicht vorhanden:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // MOCK: ohne echten Server
class ContentHubMockEnvironmentTest {
@Autowired
Environment env; // Lesen der Umgebungseigenschaften, um das Vorhandensein/Fehlen des Ports zu prüfen
@Test
void localServerPortIsNotAssigned() {
// In MOCK wird kein Port zugewiesen, daher sollte die Eigenschaft nicht vorhanden sein
Assertions.assertThat(env.getProperty("local.server.port")).isNull();
}
}
Der Web-Kontext ist hingegen vorhanden, weil die Anwendung als Web-Anwendung betrachtet wird, nur ohne echten Server. Das kann man so zeigen:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // Web-Infrastruktur vorhanden, kein Server
class ContentHubMockHasWebContextTest {
@Autowired
WebApplicationContext webContext; // In MOCK sollte der Web-Kontext erstellt werden
@Test
void webContextIsPresent() {
// Unterschied zu NONE: Web-Kontext existiert
Assertions.assertThat(webContext).isNotNull();
}
}
Warum ist dieser Modus in einem echten Projekt wie ContentHub nützlich? Weil er erlaubt, den gesamten Kontext hochzufahren, ohne den Preis eines echten Ports und eines echten Servers zu zahlen. Sie können prüfen, ob Ihre Web-Komponenten überhaupt zusammenpassen: Controller, @ControllerAdvice, Konverter, Filter, Serialisierungseinstellungen, Security-Kette – aber ohne externes Netzwerk. Dabei sollten Sie sich der Einschränkungen bewusst sein: Wenn Sie das Verhalten an der Socket-Grenze, also einen echten Client, echten HTTP-Transport, testen müssen, kann MOCK das nicht beweisen, weil es genau ohne Transport ist.
5. WebEnvironment.RANDOM_PORT – echter Server auf einem zufälligen Port
RANDOM_PORT – der Modus, in dem Spring Boot tatsächlich einen eingebetteten Server startet, aber dies so sicher wie möglich für die Umgebung macht: Der Port wird zufällig gewählt. Wenn der Port die Nummer einer Tür in einem Gebäude namens localhost ist, dann sagt RANDOM_PORT: „Geben Sie mir irgendeine freie Tür, solange es keinen Streit gibt“. Das ist nicht nur bequem; es ist eine direkte Methode, um Tests weniger anfällig und besser kompatibel mit parallelem Start lokal und in CI zu machen.
In diesem Modus erscheint local.server.port – Spring Boot trägt dort den tatsächlich belegten Port ein. Selbst wenn Sie gerade keine HTTP-Aufrufe tätigen, ist das Vorhandensein des Ports ein guter Indikator dafür, dass der Server-Modus echt ist:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // Echter Server auf zufälligem Port
class ContentHubRandomPortEnvironmentTest {
@Autowired
Environment env; // Hier wird local.server.port erscheinen
@Test
void localServerPortIsAssigned() {
// Für Server-Modi ist die Eigenschaft normalerweise vorhanden und nicht leer
Assertions.assertThat(env.getProperty("local.server.port")).isNotBlank();
}
}
Warum ist RANDOM_PORT in der Regel DEFINED_PORT vorzuziehen, wenn man sich entscheidet, den Server zu starten? Weil ein fester Port eine unnötige Fehlerquelle darstellt. Stellen Sie sich vor, auf Ihrem Rechner läuft bereits eine Anwendung auf 8080, oder ein anderer Test in einer parallelen Build-Version versucht, denselben Port zu belegen. Der Test schlägt fehl, Sie verlieren Zeit mit der Diagnose, aber die Ursache liegt nicht in der Geschäftslogik oder im Code – die Ursache ist, dass Ports in der Welt begrenzt sind und nicht gerne teilen.
Für ContentHub bedeutet dieser Modus allgemein: Die Anwendung wurde als echte Web-Anwendung mit eingebettetem Server hochgefahren, und wenn wir wollten, könnten wir mit ihr wie mit einem Live-Dienst kommunizieren. Aber im Rahmen dieser Vorlesung konzentrieren wir uns auf die Auswahl des Startmodus und nicht darauf, wie genau man Anfragen sendet: Jetzt ist es uns wichtig zu verstehen, wann der Server überhaupt existiert.
WebEnvironment.DEFINED_PORT – fester Port und seine Kosten
DEFINED_PORT – ein Modus, der sehr erwachsen klingt: fester Port, wie beim echten Start der Anwendung. Aber in einer Testumgebung bringt er fast immer mehr Schmerz als Nutzen. Das ist der Modus „Ich miete denselben Saal für die Probe und wundere mich dann, dass dort schon eine Hochzeit stattfindet“. Manchmal wird er benötigt, aber standardmäßig sollte man ihn als Ausnahme behandeln.
Der Sinn des Modus ist einfach: Spring Boot startet einen echten Server auf dem Port, der in der Konfiguration festgelegt ist, z.B. server.port=8080. Um das in einem Test zu zeigen, ist es praktisch, den Port direkt in der Annotation zu überschreiben. Das macht das Beispiel gleichzeitig autark und unterstreicht die Idee „Port ist Teil der Konfiguration“:
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, // Echter Server auf festem Port
properties = "server.port=8085" // Port explizit setzen, damit das Beispiel autark ist
)
class ContentHubDefinedPortEnvironmentTest {
@Autowired
Environment env; // Prüfen, welchen Port der Server tatsächlich belegt hat
@Test
void serverRunsOnDefinedPort() {
// In DEFINED_PORT sollte local.server.port mit server.port übereinstimmen
Assertions.assertThat(env.getProperty("local.server.port")).isEqualTo("8085");
}
}
Warum ist das anfällig? Weil 8085 belegt sein könnte. Und 8080 könnte belegt sein. Und überhaupt jeder feste Port könnte durch etwas belegt sein, das Sie gestern Abend auszuschalten vergessen haben. Infolgedessen werden Tests vom externen Zustand der Maschine abhängig. Eben deshalb wird DEFINED_PORT in den meisten Projekten, und auch in unserem Kurs, nicht als Standardwahl für Integrationstests angesehen: Er verwandelt sich zu leicht in „Tests sind dienstags nicht grün“.
6. Transaktionen und Rollback in Server-Modi
Wenn Sie von @DataJpaTest kommen, könnte sich ein sehr angenehmer Gedanke verfestigt haben: „Der Test ist zu Ende – alles wird zurückgerollt, also ist die Datenbank immer sauber“. In Data Slice ist das tatsächlich oft der Fall, weil die Testmethode in einer Transaktion ausgeführt wird und diese Transaktion zurückgerollt wird. Aber sobald Sie einen echten Server einschalten, RANDOM_PORT oder DEFINED_PORT, ergibt sich eine wichtige Weggabelung im Kopf: Wo genau wird Ihre Logik ausgeführt – im selben Thread wie der Test oder in einem separaten Thread der Anfrageverarbeitung?
In NONE und in vielen Szenarien von MOCK, wenn Sie Services direkt aus dem Test aufrufen, können Sie noch im gewohnten Modell leben: Test → Service → Repository innerhalb eines Aufrufs. Wenn Sie @Transactional auf den Test setzen und das in diesem Szenario überhaupt sinnvoll ist, können Änderungen zusammen mit der Transaktion der Testmethode zurückgerollt werden. Aber in Server-Modi ändert sich das Bild grundlegend: Die Verarbeitung der Anfrage und die damit verbundenen Transaktionen leben in der Welt des Servers, der Test dagegen in der Welt von JUnit. Das sind zwei verschiedene Ereignisströme.
Es ist hilfreich, sich das einmal so vorzustellen:
sequenceDiagram
participant T as "Test (JUnit 6)"
participant S as "Eingebetteter Server"
participant A as "Application Beans"
T->>S: "HTTP-Anfrage (in Zukunft)"
S->>A: "Anfragebearbeitung"
A->>A: "Transaktion im Service/Repository"
A-->>S: "Antwort"
S-->>T: "Antwort"
Note over T: "@Transactional am Test umfasst nicht die Transaktionen des Servers"
Auch wenn Sie jetzt keine Live-HTTP-Aufrufe schreiben, ist es wichtig, diese Idee vorher zu verinnerlichen: Server-Modi brechen die Erwartung eines ‚Rollback standardmäßig auf alles‘. Deshalb sind in vollständigen Integrationstests mit einem echten Server besonders wiederholbare Methoden zur Datenvorbereitung und -bereinigung wertvoll: @Sql, deterministische Datasets, saubere Fixture-Factories. Das ist keine Paranoia, sondern einfach eine Folge davon, dass Test und Server unterschiedliche Akteure der Geschichte sind.
Und noch eine kleine, aber praktische Schlussfolgerung: Wenn Sie sich für RANDOM_PORT oder DEFINED_PORT entscheiden, wählen Sie sie wegen ihres tatsächlichen Wertes, also zur Überprüfung des realen Runtime-Pfades, und nicht „für alle Fälle“. Wenn der reale Transport nicht benötigt wird, ist es oft vorteilhafter, zu NONE oder MOCK zurückzukehren – dort ist die Geschwindigkeit höher und das Transaktionsmodell einfacher.
7. Typische Fehler bei der Auswahl von webEnvironment
An webEnvironment ist nichts Mystisches, aber es gibt einige Fallstricke, die regelmäßig selbst diejenigen beißen, die bereits Tests schreiben können. Das Problem liegt normalerweise nicht in den Annotationen, sondern in den Erwartungen: Wir wählen den Modus aus Gewohnheit und wundern uns dann, dass er nicht das beweist, was wir dachten. Lassen Sie uns die häufigsten Stolpersteine in menschlicher Sprache durchgehen.
Fehler Nr. 1: Denken, dass @SpringBootTest standardmäßig einen echten Server startet.
Für eine MVC-Anwendung ist die Voreinstellung WebEnvironment.MOCK. Das bedeutet: Der vollständige Kontext wird hochgefahren, aber es gibt keinen Port. Wenn Sie im Kopf mit einem Server rechnen, schreiben Sie bereits einen Test mit einem falschen Modell. Eine gute Gewohnheit ist es, in wichtigen Tests den Modus explizit anzugeben, um nicht zu raten: NONE, MOCK, RANDOM_PORT oder DEFINED_PORT.
Fehler Nr. 2: MOCK in @SpringBootTest mit @WebMvcTest verwechseln.
Beide Varianten sind ohne Server, beide hängen oft mit der Idee „Controller testen“ zusammen, und daher entsteht Verwirrung. Aber @WebMvcTest ist ein schmaler Slice, bei dem Sie bewusst keine Services und Repositories vollständig hochfahren und oft Abhängigkeiten mocken. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) ist ein vollständiger Kontext mit allen Schichten. Der Unterschied im Preis und im Beweisbereich ist enorm, und es ist besser, sie als zwei verschiedene Werkzeuge zu betrachten und nicht als „fast gleiche Dinge“.
Fehler Nr. 3: DEFINED_PORT wählen, nur weil ‚das mehr nach Produktion aussieht‘.
In Tests bedeutet „mehr nach Produktion“ oft „anfälliger“. Ein fester Port ist eine Abhängigkeit vom externen Zustand der Maschine. Wenn es keinen zwingenden Grund gibt, liefert RANDOM_PORT normalerweise denselben Wert des realen Server-Pfads, aber ohne Port-Konflikt und ohne mysteriöse Ausfälle in CI.
Fehler Nr. 4: Rollback wie in @DataJpaTest in Server-Modi erwarten.
Sobald ein echter Server auftaucht, lebt die Anfrageverarbeitung getrennt von der Testmethode. Transaktionen können sich in einem anderen Thread befinden, und Ihr @Transactional am Test wird nicht zu einer magischen Kuppel über der gesamten Anwendung. Wenn man diesen Punkt nicht versteht, erhält man Tests, die „manchmal fehlschlagen“, weil Daten von vorherigen Durchläufen unerwartet übrig geblieben sind.
Fehler Nr. 5: webEnvironment aus Trägheit wählen, anstatt nach der Frage ‚welches Risiko fange ich?‘.
NONE eignet sich, wenn Web gar nicht benötigt wird; MOCK ist nützlich, wenn eine MVC-Welt ohne realen Transport benötigt wird; RANDOM_PORT wird benötigt, wenn der reale Server-Runtime-Pfad wichtig ist; DEFINED_PORT wird selten benötigt. Wenn man mit der Frage nach dem Risiko beginnt, wird die Auswahl des Modus fast mechanisch – und die Tests ähneln weniger einer zufälligen Ansammlung großer Annotationen.
GO TO FULL VERSION