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

ModeStarts a real serverUsable clientsTypical use caseCost
MOCKNoMockMvcUnit-level verification of the Controller layerLow
RANDOM_PORTYes (available port)TestRestTemplate / WebTestClientE2E verification over real HTTPHigh
DEFINED_PORTYes (fixed port)TestRestTemplate / WebTestClientExternal integration verification requiring a fixed portHigh
NONENoNoneVerification of Web-independent service layersLowest

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.