Spring offers various tools for testing our controller endpoints: MockMvc, WebTestClient, and the TestRestTemplate. While all three candidates serve a similar goal – invoking our HTTP endpoints and verifying the response – there's still a subtitle difference among them. This article will give an overview of where and how those three classes are intended to be used, including what's their differences.
TL;DR: That's the short version of the comparison:
Spring Boot Project Setup for Testing Purposes
To showcase the usage of these three different testing tools, we're going to test a basic Spring Web MVC application. We enrich this Spring Boot application with the Starter for Spring WebFlux. However, we won't mix non-blocking and blocking controller endpoints and stick to the blocking Tomcat servlet container.
The idea behind this additional dependency (Spring WebFlux) is to get access to the WebClient
and the WebTestClient
.
To demonstrate testing a controller that returns a server-side rendered view, we additionally include the Spring Boot Starter for Thymeleaf:
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 42 43 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil</groupId> <artifactId>spring-boot-test-mockmvc-webtestclient-testresttemplate</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <!-- default Maven Spring Boot build section --> </project> |
Having both the Starter for Spring Web MVC and WebFlux on the classpath, Spring Boot assumes we're going to write a blocking application and autoconfigures Tomcat for us. Nevertheless, we can use the WebClient
(the future proof version of the RestTemplate) for making HTTP requests and the WebTestClient
for testing purposes.
Throughout this article, we're going to write tests for the following sample REST API:
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 | @RestController @RequestMapping("/api/customers") public class CustomerController { private static final List<Customer> CUSTOMER_LIST = new ArrayList<>( List.of(new Customer("Duke", "Java", 42L)) ); @GetMapping public List<Customer> getAllCustomers() { return CUSTOMER_LIST; } @PostMapping public ResponseEntity<Void> createCustomer( @RequestBody Customer customer, UriComponentsBuilder uriComponentsBuilder) { CUSTOMER_LIST.add(customer); return ResponseEntity .created(uriComponentsBuilder.path("/api/customers/{id}") .buildAndExpand(customer.id()).toUri()) .build(); } } |
The CustomerController
allows us to create customers and return all existing customers. For the sake of simplicity, we're storing all customers inside an in-memory List
. As this article is all about testing our Spring Boot application via our controller endpoints, the upcoming explanations hold true independent of what's our actual data store.
Next to this @RestController
, our sample project exposes a server-side rendered view using Thymeleaf:
1 2 3 4 5 6 7 8 9 10 | @Controller @RequestMapping("/admin") public class AdminController { @GetMapping public String getAdminView(Model model) { model.addAttribute("secretMessage", "Duke 42"); return "admin"; } } |
This endpoint renders a view and returns HTML instead of a JSON payload.
With these two controller classes in place, let's explore the differences of MockMvc
, the WebTestClient
and the TestRestTemplate
.
Using MockMvc for Testing Spring Boot Applications
At its core, MockMvc
is a mocked servlet environment. As the name (MockMvc) applies, we use it for servlet-based Spring Web MVC controller endpoints. But not for Spring WebFlux endpoints.
In comparison to a real servlet environment, with MockMvc
, we don't need to start our embedded servlet container (e.g., Tomcat). Hence we don't occupy any port. We use the MockMvc
instance to interact with this mocked environment and don't initiate real HTTP communication.
This mocked servlet environment still follows HTTP semantics as we can modify the headers and body of the request (MockMvcRequest
) and can inspect the response's header, status, and body.
We can either bootstrap this environment on our own (see the different bootstrap variants) or use the autoconfigured version from Spring Boot (@AutoConfigureMockMvc
).
Most of the time, we'll see this class is in combination with @WebMvcTest
– a sliced Spring Boot test annotation – for our Spring Web MVC endpoints. The @WebMvcTest
meta-annotation contains the @AutoConfigureMockMvc
annotation, and hence we get the MockMvc
instance autoconfigured for such tests.
Let's start with a first test for our CustomerController
where we want to verify the @PostMapping
endpoint using MockMvc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @WebMvcTest(CustomerController.class) class CustomerControllerMockMvcTest { @Autowired private MockMvc mockMvc; @Test void shouldCreateNewCustomers() throws Exception { this.mockMvc .perform(post("/api/customers") .contentType(APPLICATION_JSON) .content(""" { "firstName": "Mike", "lastName": "Thomson", "id": 43 } """) ) .andExpect(status().isCreated()); } } |
First, we inject the autoconfigured version of MockMvc
to our test as we're using @WebMvcTest
. This test creates a minimal subset of our Spring ApplicationContext
containing only Spring Web MVC-related beans and our CustomerController
.
Next, we use the injected MockMvc
instance to perform a POST request against our mocked servlet environment. We prepare the request's body to contain a valid JSON payload. As a basic verification, we expect our endpoint to return 201.
We get access to a fluent API to perform various operations against our mocked servlet environment and can simulate incoming HTTP requests (GET, POST, PUT, etc.). Furthermore, we can chain verifications to our request to verify the body, header, etc. of the MockMvc
response.
However, we have to keep in mind that we're not making any requests against a running servlet container. We operate in a mocked environment that is less close to our production runtime. No actual HTTP request hit our endpoint.
MockMvc
also comes with support for testing server-side rendered views:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @WebMvcTest(AdminController.class) class AdminControllerMockMvcTest { @Autowired private MockMvc mockMvc; @Test void shouldReturnAdminView() throws Exception { this.mockMvc .perform(get("/admin")) .andExpect(status().is(200)) .andExpect(view().name("admin")) .andExpect(model().attributeExists("secretMessage")); } } |
In the example above, we get rather low-level access to the response and Spring's internals as we can verify both the returned view and its model. This allows testing if the Thymeleaf engine can successfully render our view indicating that we set all the mandatory model attributes.
For further hands-on examples, including how to verify protected endpoints by Spring Security, head over to this @WebMvcTest and MockMvc introduction article.
Using the WebTestClient for Testing Spring Boot Applications
As the name applies, the WebTestClient
is the testing counterpart of the Spring Webflux WebClient
.
Although its non-blocking origin, we can still use it for blocking applications. The primary purpose is to verify controller endpoints of Spring WebFlux applications in combination with @WebFluxTest
.
However, the Spring team has extended the use cases for the WebTestClient
. We can use it for both tests that work with a mocked-servlet environment (MockMvc) and for integration tests against a running servlet container.
Let's see this in action by writing an integration test for our CustomerController
endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class CustomerControllerWebTestClientTest { @Autowired private WebTestClient webTestClient; @Test void shouldCreateNewCustomers() { this.webTestClient .post() .uri("/api/customers") .bodyValue(""" { "firstName": "Mike", "lastName": "Thomson", "id": 43 } """) .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE) .exchange() .expectStatus() .isCreated(); } } |
As soon as we're starting our embedded servlet container for our @SpringBootTest and have Spring WebFlux on the classpath, Spring Boot autoconfigures a WebTestClient
for us. This autoconfigured instance targets our locally running Spring Boot application.
What follows is a test for our HTTP POST API endpoint to verify our customer creation use case. We can chain both the request setup and the verification thanks to the fluent API of the WebTestClient
.
In this example, we initiate a real HTTP request that reaches our locally running Spring Boot application.
Starting with Spring Boot 2.4 (Spring Framework 5.3), we can now also use the WebTestClient
to perform requests and verify their response when targeting a MockMvc
environment. This gives us the possibility to potentially reuse a request specification for our MockMvc
test and integration tests against a running servlet container.
Using the TestRestTemplate for Testing Spring Boot Applications
The last candidate for our comparison is the TestRestTemplate
. It's the testing counterpart of the RestTemplate
.
Similar to the WebTestClient
, Spring Boot autoconfigures the TestRestTemplate
as soon as we're starting an embedded servlet container (see @SpringBootTest configuration options).
Let's repeat the test setup for the HTTP POST endpoint of our CustomerController
using the TestRestTemplate
:
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 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class CustomerControllerTestRestTemplateTest { @Autowired private TestRestTemplate testRestTemplate; @Test void shouldCreateNewCustomers() { HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE); HttpEntity<String> requestEntity = new HttpEntity<>(""" { "firstName": "Mike", "lastName": "Thomson", "id": 43 } """, requestHeaders ); ResponseEntity<Void> response = this.testRestTemplate .exchange("/api/customers", HttpMethod.POST, requestEntity, Void.class); assertThat(response.getStatusCodeValue()) .isEqualTo(201); } } |
We're posting a valid payload to our API endpoint and expect an HTTP status of 201 Created. Similar to the previous WebTestClient
example, we invoke our controller with an HTTP request.
Compared to the WebTestClient
, the usage of the TestRestTemplate
is a bit less fluent. Constructing the HttpHeaders
and HttpEntity
object before passing them to the exchange method, adds some boilerplate code to our test. Furthermore, we can't fluently write assertions for the response headers, body, and status code.
It achieves the same purpose as the reactive test HTTP client and lets us target our locally running Tomcat server (usually on a random port) for testing purposes.
Unlike the WebTestClient
, we can use the TestRestTemplate
only for blocking Spring Web MVC endpoints. We can't use this for testing Spring WebFlux endpoints that return Flux
or Mono
data types.
Furthermore, we can't use the TestRestTemplate
for interacting with a mocked servlet environment.
Summary: MockMvc vs. WebTestClient vs. TestRestTemplate
All three tools help us invoke and test our Spring Boot application's endpoint. The main difference lies in whether we can perform requests against a mocked servlet environment and/or our servlet container runtime. Furthermore, not all tools are designed to work with both Spring Web MVC (blocking) and Spring WebFlux (non-blocking).
In short, these three technologies serve the following purpose:
MockMvc
: Fluent API to interact with a mocked servlet environment. No real HTTP communication. The perfect solution to verify blocking Spring WebMVC controller endpoints. We either bootstrap theMockMvc
instance on our own, use@WebMvcTest
or@SpringBootTest
(without a port configuration). Includes API support for verifying the model or view name of a server-side rendered view endpoint.WebTestClient
: Originally the testing tool for invoking and verifying Spring WebFlux endpoints. However, we can also use it to write tests for a running servlet container orMockMvc
. Fluent API that allows chaining the request and verification. There's no API support for verifying the model or view name of a server-side rendered view endpoint.TestRestTemplate
: Test and verify controller endpoints for a running servlet container over HTTP. Less fluent API. If our team is still familiar with theRestTemplate
and hasn't made the transition to theWebTestClient
(yet), we may favor this for our integration tests. We can't use theTestRestTemplate
to interact with mocked servlet environment or Spring WebFlux endpoints. There's no API support for verifying the model or view name of a server-side rendered view endpoint.
When in doubt or when starting a new project, I prefer to use MockMvc
for the @WebMvcTest
tests and the WebTestClient
for integration tests.
You can find related articles here:
- Testing Spring Boot Applications With REST Assured
- Test Your Spring MVC Controller with the WebTestClient and MockMvc
- Testing Spring Boot Applications With MockMvc and @WebMvcTest
- Write Integration Tests For Your Spring WebSocket Endpoints
- Spring WebTestClient for Efficient REST API Testing
The source code for this example is available on GitHub.
Joyful testing,
Philip