Once you’ve got the basics of JUnit and Mockito down, it’s tempting to just write every @SpringBootTest with RANDOM_PORT. But without paying attention to webEnvironment, you end up accumulating unnecessarily heavy tests.
This article organizes what each of the four webEnvironment modes does and doesn’t start up, using a comparison table and working code, so you can choose the lightest mode suited to what you’re testing. The basics of @SpringBootTest itself are covered in the article above, so here we’ll focus solely on webEnvironment.
The default for webEnvironment is MOCK
webEnvironment is an option that determines how @SpringBootTest starts the ApplicationContext, and if not specified, the default is MOCK. If you change modes based on the assumption that “RANDOM_PORT is always safe,” you end up paying the cost of starting a real server every time.
@SpringBootTest // Equivalent to webEnvironment = WebEnvironment.MOCK
class OrderServiceDefaultTest {
}
Comparison table of the four modes
| Mode | Starts a real server | Usable clients | Typical use case | Cost |
|---|---|---|---|---|
| MOCK | No | MockMvc | Unit-level verification of the Controller layer | Low |
| RANDOM_PORT | Yes (available port) | TestRestTemplate / WebTestClient | E2E verification over real HTTP | High |
| DEFINED_PORT | Yes (fixed port) | TestRestTemplate / WebTestClient | External integration verification requiring a fixed port | High |
| NONE | No | None | Verification of Web-independent service layers | Lowest |
From this alone, a policy emerges: “if you’re not touching the Web layer, use NONE; if you’re only looking at the Controller, use MOCK.”
Verifying the Controller layer with MOCK
This verifies through MockMvc without starting a real server. Since no actual port is opened, @LocalServerPort cannot be used.
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerMockTest {
@Autowired MockMvc mockMvc;
@Test
void returnsOrderDetail() throws Exception {
mockMvc.perform(get("/api/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("PAID"));
}
}
Most business logic verification can be handled here. For details on MockMvc itself, see Controller testing with @WebMvcTest.
Verifying real-server HTTP communication with RANDOM_PORT
Use this when you need to verify things that can only be confirmed by going through actual HTTP, such as redirects or cookies. Since an available port is automatically assigned, there’s no conflict even with parallel execution.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApiRandomPortTest {
@LocalServerPort int port;
@Autowired TestRestTemplate restTemplate;
@Test
void createsOrder() {
var response = restTemplate.postForEntity(
"http://localhost:" + port + "/api/orders", new OrderRequest("item-1"), OrderResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
}
If you’d rather write it with WebTestClient, attach @AutoConfigureWebTestClient and inject it. You can also write response body assertions via method chaining.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class OrderApiWebTestClientTest {
@Autowired WebTestClient webTestClient;
@Test
void createsOrder() {
webTestClient.post().uri("/api/orders")
.bodyValue(new OrderRequest("item-1"))
.exchange()
.expectStatus().isCreated()
.expectBody().jsonPath("$.status").isEqualTo("CREATED");
}
}
This is covered in more detail in E2E testing with TestRestTemplate.
Use DEFINED_PORT only when a fixed port is required
Use DEFINED_PORT only in limited cases where an external system calls in assuming a predetermined port number — for example, when a payment gateway service has a webhook callback destination registered against a fixed port.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@TestPropertySource(properties = "server.port=8443")
class PaymentWebhookDefinedPortTest {
@Autowired TestRestTemplate restTemplate;
@Test
void acceptsCallbackOnFixedPort() {
var response = restTemplate.postForEntity(
"http://localhost:8443/api/webhooks/payment", new WebhookPayload("paid"), Void.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
The downside is that tests using the same port tend to conflict under parallel execution, and in most cases RANDOM_PORT can serve as a substitute, so its use should be kept to a minimum.
NONE doesn’t start any Web environment at all
If you’re only verifying collaboration at the service or repository layer, NONE is the lightest option. Neither MockMvc, TestRestTemplate, nor WebTestClient can be used.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class OrderServiceNoneTest {
@Autowired OrderService orderService;
@Test
void calculatesTotalPrice() {
Order order = orderService.create(new OrderRequest("item-1"));
assertThat(order.getTotalPrice()).isEqualTo(1000);
}
}
If you choose MOCK for verification that doesn’t use the Web layer at all, you’re only losing by unnecessarily starting up a mock servlet environment. In this case, prefer NONE.
Replacing external dependencies with @MockitoBean
@MockitoBean is an annotation introduced in Spring Framework 6.2 (Spring Boot 3.4) and later, as the successor to the now-deprecated @MockBean. If you’re using an older version, read this section’s @MockitoBean as @MockBean instead.
If you can mock external API or DB access, there are more cases where you can verify things without starting a real server.
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerWithExternalApiTest {
@MockitoBean PaymentGatewayClient paymentGatewayClient;
@Autowired MockMvc mockMvc;
@Test
void returnsPaymentFailure() throws Exception {
when(paymentGatewayClient.charge(any())).thenThrow(new PaymentDeclinedException());
mockMvc.perform(post("/api/orders/1/pay")).andExpect(status().isPaymentRequired());
}
}
The criterion for judging whether a real server is truly necessary is whether you want to verify the actual HTTP behavior over the network itself. If you only need to replace the results of external dependencies, the combination of @MockitoBean and MOCK is usually sufficient.
Organizing by scenario with @Nested
As scenarios with different webEnvironment or mock configurations increase, splitting them up with @Nested makes things easier to read.
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerScenarioTest {
@Autowired MockMvc mockMvc;
@Nested
class WhenPaymentSucceeds {
@MockitoBean PaymentGatewayClient client;
@Test
void returnsCreated() throws Exception {
when(client.charge(any())).thenReturn(new PaymentResult("OK"));
mockMvc.perform(post("/api/orders/1/pay")).andExpect(status().isCreated());
}
}
@Nested
class WhenPaymentFails {
@MockitoBean PaymentGatewayClient client;
@Test
void returnsPaymentRequired() throws Exception {
when(client.charge(any())).thenThrow(new PaymentDeclinedException());
mockMvc.perform(post("/api/orders/1/pay")).andExpect(status().isPaymentRequired());
}
}
}
Rather than mixing mock configurations for the success and failure paths in a single class, separating them by purpose with @Nested gives you a clearer picture.
Cases where starting a real server is truly necessary
RANDOM_PORT and DEFINED_PORT should only be used in cases like the following.
- You want to verify the actual behavior of an HTTP client going through a real network — redirects, cookies, response headers, and the like
- You want to confirm behavior that depends on the servlet container’s error-handling mechanism, such as Spring Boot’s default error page forwarding (internal forward to
/error) — note that the behavior of Filter Beans themselves is already fully applied by default under@AutoConfigureMockMvc, so MOCK is sufficient if you’re only verifying the filter - You want to confirm integration with an external system that assumes an actual port and protocol
For most other cases — verifying business logic or the input/output of the Controller layer — MOCK or NONE is sufficient.
Summary
When choosing webEnvironment, consider this order of priority: NONE if you don’t use the Web layer, MOCK if you only need the Controller layer, and RANDOM_PORT if you need to verify actual HTTP behavior. Replacing external dependencies with @MockitoBean beforehand widens the room for choosing a lighter mode.
If you’d like to review the full picture of annotations, check out the test annotation cheat sheet, and if you need integration tests that go all the way through the DB, see Integration testing with Testcontainers as well.