Starting with Spring Framework version 5.3 (part of Spring Boot since version 2.4.0) you can perform requests with the WebTestClient against MockMvc. This allows unifying the way you invoke your controller endpoints during tests. For both test scenarios (real HTTP communication or mocked Servlet environment), you now have the possibility to use the WebTestClient
.
WebTestClient and MockMvc before Spring 5.3
In the past (before Spring Framework 5.3), you either tested your MVC controller endpoints with MockMvc
or WebTestClient
(or the TestRestTemplate
).
Using MockMvc
you could (and still can) test your MVC components with a mocked Servlet environment. You get an auto-configured MockMvc
instance when using @WebMvcTest
. With such tests you can verify that e.g. an endpoint is properly protected by Spring Security, the correct HTTP status code is returned on failure or the Model
contains all required attributes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @WebMvcTest(UserController.class) class UserControllerMockMvcTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldForbidAccessToUnauthenticatedRequests() throws Exception { this.mockMvc .perform(MockMvcRequestBuilders.get("/api/users")) .andExpect(status().is4xxClientError()); } @Test @WithMockUser(username = "duke") void shouldReturnListOfUsersForAuthenticatedRequests() throws Exception { when(userService.getAllUsers()) .thenReturn(List.of(new User(42L, "duke"), new User(24L, "mike"))); this.mockMvc .perform(MockMvcRequestBuilders.get("/api/users")) .andExpect(status().isOk()) .andExpect(jsonPath("$.size()", Matchers.is(2))); } } |
All of the verification above is happening without any underlying HTTP communication. This is great when it comes to performance, as these tests only use a sliced context and don't have to start the embedded Servlet container. On the other side, such tests don't exactly mirror how your application is invoked in production.
This brings us to the WebTestClient
.
Spring Boot autoconfigures an instance of the WebTestClient
whenever you select WebEnvironment.RANDOM_PORT
or WebEnvironment.DEFINED_PORT
for @SpringBootTest
. This ensures to start the embedded Servlet container (e.g. Tomcat) on a local port and hence you can communicate with your application over HTTP.
The auto-configured WebTestClient
is bound (correct base URL + port) to your application and you can start writing tests without further configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class ApplicationTests { @Autowired private WebTestClient webTestClient; @Test void contextLoads() { this.webTestClient .get() .uri("/api/users") .exchange() .expectStatus().is4xxClientError(); } } |
While you used the WebTestClient
and MockMvc
separately in the past, you can now (with Spring Framework 5.3) perform requests with the WebTestClient
against a MockMvc
instance.
Perform requests with WebTestClient against MockMvc
Let's see how we can configure the WebTestClient
to target MockMvc
.
As of now, there is no auto-configuration for this test setup and you have to create the WebTestClient
instance on your own. The MockMvcTestClient
interface provides static factory methods to create a client that is bound to either specific controllers, an application context, or an MockMvc
instance.
While creating the client, you can apply any customizations e.g. default headers, filter functions, etc.
1 2 3 4 5 6 7 8 | @BeforeEach public void setup() { this.webTestClient = MockMvcWebTestClient .bindTo(mockMvc) .defaultHeader("X-Duke", "42") .filter(logRequest()) .build(); } |
What's left is to perform your requests using the WebTestClient
and write expectations on the result:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | @WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; private WebTestClient webTestClient; @BeforeEach public void setup() { this.webTestClient = MockMvcWebTestClient .bindTo(mockMvc) .defaultHeader("X-Duke", "42") .filter(logRequest()) .build(); } @Test @WithMockUser(username = "duke") void shouldReturnListOfUsersForAuthenticatedRequests() { when(userService.getAllUsers()) .thenReturn(List.of(new User(42L, "duke"), new User(24L, "mike"))); this.webTestClient .get() .uri("/api/users") .exchange() .expectStatus().is2xxSuccessful() .expectBody().jsonPath("$.size()", Matchers.is(2)); } private ExchangeFilterFunction logRequest() { return (clientRequest, next) -> { System.out.printf("Request: %s %s %n", clientRequest.method(), clientRequest.url()); return next.exchange(clientRequest); }; } } |
As you see in the test above, the usage of the WebTestClient
is similar to writing a test against a running Spring Boot application and invoke a controller over HTTP. But with the test above there is no HTTP communication happening as we target MockMvc
.
Testing Spring @Controller endpoints with this setup
You might wonder: “What about testing @Controller
endpoints? I thought the WebTestClient
only offers expectations for the body, header, or status of a real HTTP response.”
Fortunately, the Spring Team also added support for this and you can use this setup for testing your Thymeleaf Spring Boot application. If you use the WebTestClient
and write tests against a MockMvc
instance, you are now able to write assertions for e.g. the Model
or RedirectAttributes
.
The WebTestClient
doesn't offer methods for such assertions itself. Once you have access to the EntityExchangeResult
(calling .returnResult()
), you can use MockMvcWebTestClient.resultActionsFor()
and write expectations similar to MockMvc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Test void shouldReturnDashboardViewWithDefaultModel() throws Exception { EntityExchangeResult<byte[]> result = this.webTestClient.get() .uri("/dashboard") .exchange() .expectStatus().is2xxSuccessful() .expectBody().returnResult(); MockMvcWebTestClient.resultActionsFor(result) .andExpect(model().size(2)) .andExpect(model().attributeExists("message")) .andExpect(model().attributeExists("orderIds")) .andExpect(model().attribute("message", "Hello World!")) .andExpect(view().name("dashboard")); } |
Summary
With this small enhancement, you are now able to unify the way you invoke your controller endpoints during tests. This might help to standardize and reuse existing WebTestClient
snippets for tests using @WebMvcTest
and MockMvc
.
IMHO this is not (yet) a game-changer feature, especially if you are using Kotlin and are familiar with the MockMvc
DSL. When it comes to mocking security concerns, the MockMvc
integration with Spring Security is quite powerful due to the SecurityMockMvcRequestPostProcessors
. I couldn't find any similar feature for the WebTestClient
(yet), as webTestClient.mutateWith(mockUser())
only works (AFAIK) when targeting a reactive WebFlux environment.
You can find the source code with further WebTestClient and MockMvc examples on GitHub.
Have fun performing requests with the WebTestClient
against MockMvc
,
Philip