Skip to content

Testing Web Components

The web layer is tested with WebTestClient, which mirrors the WebClient API and adds response assertions. This page covers testing annotated controllers, exception handling, RouterFunctions, and filters — with the dependencies mocked or spied through Spring's bean overriding annotations.

The tests run on JUnit 5 with Mockito for stubbing; for the Kotlin equivalents see Testing in Kotlin.

A controller test with a mocked dependency

@WebFluxTest(controllers = ...) loads only the web configuration for the given controller — no database, no full context. The controller's dependencies are replaced with @MockitoBean (from org.springframework.test.context.bean.override.mockito), and Mockito stubs their publisher-returning methods:

@WebFluxTest(controllers = PostController.class)
public class PostControllerTest {

    @Autowired
    WebTestClient client;

    @MockitoBean
    PostRepository posts;

    @Test
    public void getAllPosts() {
        when(this.posts.findAll()).thenReturn(Flux.just(
                Post.builder().id(UUID.randomUUID()).title("post one").content("content of post one").build(),
                Post.builder().id(UUID.randomUUID()).title("post two").content("content of post two").build()
        ));
        client.get().uri("/posts")
                .exchange()
                .expectStatus().isOk()
                .expectBody().jsonPath("$.size()").isEqualTo(2);

        verify(this.posts, times(1)).findAll();
        verifyNoMoreInteractions(this.posts);
    }
}

Notes:

  • @WebFluxTest auto-configures a WebTestClient bound to the mock web environment — inject it with @Autowired.
  • @MockitoBean replaces the bean in the context with a Mockito mock; when(...)/given(...) stub the reactive methods to return Flux.just(...) or Mono.just(...).
  • The BDD style works too: given(this.posts.findAll()).willReturn(Flux.just(post1, post2)).

Boot 3 vs Boot 4

Spring Boot 3 used @MockBean (deprecated and removed in Boot 4); Spring Framework 6.2+ / Boot 4 use @MockitoBean and @MockitoSpyBean from org.springframework.test.context.bean.override.mockito.

Mocks and spies

  • @MockitoBean — replace a bean entirely with a mock. Unstubbed publisher-returning methods return empty publishers.
  • @MockitoSpyBean — wrap the real bean in a spy: real methods run unless explicitly stubbed, and interactions are still verifiable. Use it when the collaborator's real behavior matters and you only need to observe or tweak a few calls:
@MockitoSpyBean
PostRepository posts;

Both annotations live in the same package as @MockitoBean and work in any Spring test — slices, @SpringBootTest, or plain SpringJUnitConfig tests.

Exception handling and status codes

Exception handlers are exercised by driving the client into the error path and asserting the mapped status — here a not-found domain exception maps to 404 through the controller's @ExceptionHandler:

@Test
fun `get post that not found`() = runTest {
    val id = 1L
    coEvery { postRepository.findById(any()) } throws PostNotFoundException(id)

    client.get().uri("/$id")
        .exchange()
        .expectStatus().isNotFound

    coVerify(exactly = 1) { postRepository.findById(any()) }
}

The Java pattern is identical with Mockito: stub the dependency to return Mono.error(new PostNotFoundException(id)), then expectStatus().isNotFound(). Assert the body of RFC 9457 ProblemDetail responses with expectBody(ProblemDetail.class) when a handler returns one.

Testing RouterFunctions

Functional routes are tested by binding the client to the RouterFunction bean. From the boot-start-routes sample:

@AutoConfigureWebTestClient
@SpringBootTest
// create a `WebTestClient` with essential options from application context automatically,
// it works on both mock env or a real server.
public class ApplicationTest {

    @Autowired
    RouterFunction<ServerResponse> routerFunction;

    @MockitoBean
    PostRepository posts;

    WebTestClient client;

    @BeforeEach
    void setUp() {
        this.client = WebTestClient.bindToRouterFunction(this.routerFunction)
                .configureClient()
                .build();
    }

    @Test
    public void getAllPosts() {
        // stub the repository, call client.get().uri("/posts")...
    }
}

bindToRouterFunction handles a single RouterFunction; bindToApplicationContext(context) loads the full handler infrastructure (filters, exception handlers) without a socket, and bindToWebHandler(...) sits between the two.

Testing filters

Filters are verified by importing them into a @WebFluxTest slice and asserting the outcome with and without the triggering condition — for example, a security-check filter that requires a user query parameter:

@WebFluxTest(controllers = HelloController.class)
class WebFilterTests {

    @TestConfiguration
    @Import(value = SecurityWebFilter.class)
    class TestConfig

    @Autowired
    WebTestClient client;

    @Test
    void `authentication failed with no user params`() = runTest {
        client.get().uri("/hello")
            .exchange()
            .expectStatus().isUnauthorized
    }

    @Test
    void `authentication passed with user params`() = runTest {
        client.get().uri("/hello?user=Hantsy")
            .exchange()
            .expectStatus().isOk
    }
}

A Java WebFilter test follows the same shape: @Import the filter, request a route the filter protects, assert 401 without the precondition and 200 with it.

Against a running server

Slice tests exercise the request-handling stack only. When a test needs the real HTTP contract — SSE, WebSocket, cookies — bind the client to a live server instead; see Spring Test Context and Slices.

Mocking a target HTTP API with WireMock

The mirror image of the tests above: when your application is the client, stub the remote API with WireMock and assert the reactive call chain. The client sample tests its PostClient wrapper with @WireMockTest(httpPort = 8080) — the same port the client targets — and StepVerifier for the assertions:

@SpringJUnitConfig(classes = { ClientConfig.class, Jackson2ObjectMapperConfig.class })
@WireMockTest(httpPort = 8080)
public class PostClientTest {

    @Autowired
    PostClient postClient;

    @SneakyThrows
    @Test
    public void testGetAllPosts() {
        var data = List.of(
                new Post(UUID.randomUUID(), "title1", "content1", Status.DRAFT, LocalDateTime.now()),
                new Post(UUID.randomUUID(), "title2", "content2", Status.PUBLISHED, LocalDateTime.now())
        );
        stubFor(get("/posts")
                .willReturn(aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withResponseBody(Body.fromJsonBytes(Json.toByteArray(data)))));

        postClient.allPosts()
                .as(StepVerifier::create)
                .expectNextCount(2)
                .verifyComplete();

        verify(getRequestedFor(urlEqualTo("/posts"))
                .withHeader("Accept", equalTo("application/json")));
    }

    @SneakyThrows
    @Test
    public void testCreatePost() {
        var id = UUID.randomUUID();
        var data = new Post(null, "title1", "content1", Status.DRAFT, null);

        stubFor(post("/posts")
                .willReturn(aResponse()
                        .withHeader("Location", "/posts/" + id)
                        .withStatus(201)));

        postClient.save(data)
                .as(StepVerifier::create)
                .consumeNextWith(entity -> {
                    assertThat(entity.getHeaders().getLocation().toString()).isEqualTo("/posts/" + id);
                    assertThat(entity.getStatusCode().value()).isEqualTo(201);
                })
                .verifyComplete();
    }
}

Reading the wiring:

  • @WireMockTest(httpPort = 8080) starts WireMock on the port the WebClient under test points at — no code changes between test and production.
  • stubFor(...) registers the expected request and response; verify(...) asserts the client actually sent the expected headers and payload.
  • The reactive result is asserted with .as(StepVerifier::create) — see Testing for the signal-level assertions.

The dependency is com.github.tomakehurst:wiremock-standalone (test scope). You can explore the complete codes in the client sample's PostClientTest.