Did you ever found yourself saying: I usually ignore testing my endpoints because the security setup is hard. That belongs to the past. With MockMvc, Spring provides a great tool for testing Spring Boot applications. This guide gives you recipes at hand to verify your @Controller
and @RestController
endpoints and other relevant Spring Web MVC components.
Required Maven dependencies
It is very little you need to start testing your controller endpoints using MockMvc
. Including both the Spring Boot Starter Web and the Spring Boot Starter Test (aka. swiss-army for testing Spring Boot applications) is everything you need:
1 2 3 4 5 6 7 8 9 10 | <dependency> <groupId>org.springframework.boot</gro<code>upId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> |
Once you secure your endpoints using Spring Security, the following test dependency helps a lot when testing protected controller:
1 2 3 4 5 | <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> |
What can we test and verify with MockMvc?
That's easy to answer: Everything related to Spring MVC!
This includes your controller endpoints (both @Controller
and @RestController
) and any Spring MVC infrastructure components like Filter
, @ControllerAdvice
, WebMvcConfigurer
, etc.
Possible test scenarios can be the following:
- Does my REST API controller return a proper JSON response?
- Is the model for my Thymeleaf view properly initialized?
- Does my endpoint return the HTTP status code 418 if my service layer throws a
TeaNotFoundException
? - Is my REST API endpoint properly protected with Basic Auth?
- Can only users with the role ADMIN access the DELETE endpoint?
- etc.
With MockMvc
we perform requests against a mocked servlet environment. There won't be any real HTTP communication during our tests. We rather directly work with the mocked environment provided by Spring. MockMvc
acts as the entry-point to this mocked servlet environment. Similar to the WebTestClient
when accessing our started Servlet container over HTTP.
MockMvc setup
There are two ways to create a MockMvc
instance: using auto-configuration or hand-crafted.
Following Spring Boot's auto-configuration principle, you only need to annotate your test with @WebMvcTest
. This annotation not only ensures to auto-configure MockMvc
but also create a sliced Spring context containing only MVC related beans.
Make sure to pass the class name of the controller you want to test alongside this annotation: @WebMvcTest(MyController.cass)
. Otherwise, Spring will create a context including all your controller endpoints.
The second approach is helpful when your application uses plain Spring without Spring Boot or you want to fine-tune the setup:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class TaskControllerTest { private MockMvc mockMvc; @BeforeEach public void setup() { this.mockMvc = MockMvcBuilders.standaloneSetup(new TaskController(new TaskService())) .setControllerAdvice() .setLocaleResolver(localResolver) .addInterceptors(interceptorOne) .build(); } } |
or …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @WebMvcTest(TaskController.class) public class TaskControllerSecondTest { @Autowired private WebApplicationContext context; @MockBean private TaskService taskService; protected MockMvc mockMvc; @BeforeEach public void setup() { this.mockMvc = MockMvcBuilders .webAppContextSetup(this.context) .apply(springSecurity()) .build(); } } |
Invoke and test an API endpoint with MockMvc
Let's start with the first test: Ensuring the JSON result from a @RestController
endpoint is correct.
The controller class has three endpoint mappings and is straightforward:
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 | @Validated @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping public List<User> getAllUsers() { return this.userService.getAllUsers(); } @GetMapping @RequestMapping("/{username}") public User getUserByUsername(@PathVariable String username) { return this.userService.getUserByUsername(username); } @PostMapping public ResponseEntity<Void> createNewUser(@RequestBody @Valid User user, UriComponentsBuilder uriComponentsBuilder) { this.userService.storeNewUser(user); return ResponseEntity .created(uriComponentsBuilder.path("/api/users/{username}").build(user.getUsername())) .build(); } } |
With a first test, we want to ensure the JSON payload from /api/users
is what we expect.
As our UserController
has a dependency on a UserService
bean, we'll mock it. This ensures we can solely focus on testing the web-layer and don't have to provide further infrastructure for our service classes to work (e.g. remote systems, databases, etc.).
The minimal test setup looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 | @WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; // ... upcoming test } |
Before we invoke the endpoint using MockMvc
, we have to mock the result of our UserService
. Therefore we can return a hard-coded list of users:
1 2 3 4 5 6 7 8 9 10 11 12 | @Test public void shouldReturnAllUsersForUnauthenticatedUsers() throws Exception { when(userService.getAllUsers()) this.mockMvc .perform(MockMvcRequestBuilders.get("/api/users")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.size()").value(1)) .andExpect(MockMvcResultMatchers.jsonPath("$[0].username").value("duke")) } |
Next, we use MockMvcRequestBuilders
to construct our request against the mocked servlet environment. This allows us to specify the HTTP method, any HTTP headers, and the HTTP body.
Once we invoke our endpoint with perform
, we can verify the HTTP response using fluent-assertions to inspect: headers, status code, and the body.
JsonPath is quite helpful here to verify the API contract of our endpoint. Using its standardized expressions (somehow similar to XPath for XML) we can write assertions for any attribute of the HTTP response.
Our next test focuses on testing the HTTP POST endpoint to create new users. This time we need to send data alongside our MockMvc
request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Test public void shouldAllowCreationForUnauthenticatedUsers() throws Exception { this.mockMvc .perform( post("/api/users") .contentType(MediaType.APPLICATION_JSON) .with(csrf()) ) .andExpect(status().isCreated()) .andExpect(header().exists("Location")) .andExpect(header().string("Location", Matchers.containsString("duke"))); verify(userService).storeNewUser(any(User.class)); } |
To avoid an HTTP 403 Forbidden response, we have to populate a valid CsrfToken
for the request. This only applies if your project includes Spring Security and CSRF is enabled (which you always should). Due to the great MockMvc and Spring Security integration, we can create this token using .with(csrf())
.
Writing tests for a Thymeleaf controller
There is more to Spring MVC than writing API endpoints: exposing server-side rendered views following the MVC (Model View Controller) pattern.
Our application exposes one Thymeleaf view, that is also pre-filled with default data for the Model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | @Controller @RequestMapping("/dashboard") public class DashboardController { private final DashboardService dashboardService; public DashboardController(DashboardService dashboardService) { this.dashboardService = dashboardService; } @GetMapping public String getDashboardView(Model model) { model.addAttribute("user", "Duke"); model.addAttribute("analyticsGraph", dashboardService.getAnalyticsGraphData()); model.addAttribute("quickNote", new QuickNote()); return "dashboard"; } } |
Now it would be great if we can verify that our model is present and we are returning the correct view (locatable & renderable by Spring MVC). Writing a web-test with Selenium for this scenario would be quite expensive (time & maintenance effort).
Fortunately MockMvc
provides verification mechanisms for this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @WebMvcTest(DashboardController.class) class DashboardControllerTest { @Autowired private MockMvc mockMvc; @MockBean private DashboardService dashboardService; @Test public void shouldReturnViewWithPrefilledData() throws Exception { when(dashboardService.getAnalyticsGraphData()).thenReturn(new Integer[]{13, 42}); this.mockMvc .perform(get("/dashboard")) .andExpect(status().isOk()) .andExpect(view().name("dashboard")) .andExpect(model().attribute("user", "Duke")) .andExpect(model().attribute("analyticsGraph", Matchers.arrayContaining(13, 42))) .andExpect(model().attributeExists("quickNote")); } } |
The MockMvc
request setup looks similar to the tests in the last section. What's different is the way we assert the response. As this endpoint returns a view rather than JSON, we make use of the ResultMatcher
.model()
.
And can now write assertions for the big M in MVC: the Model
.
There is also a ResultMatcher
available to ensure any FlashAttributues
are present if you follow the POST – redirect – GET pattern.
Test a secured endpoint with Spring Security and MockMvc
Let's face reality, most of the time our endpoints are protected by Spring Security. Neglecting to write tests because the security setup is hard, is foolish. Because it isn't. The excellent integration of MockMvc
and Spring Security ensures this.
Once Spring Security is available on the classpath, MockMvc
will auto-configure your security config.
As a demo, let's consider the following security configuration for our MVC application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authorize -> authorize .mvcMatchers(HttpMethod.GET, "/dashboard").permitAll() .mvcMatchers(HttpMethod.GET, "/api/tasks/**").authenticated() .mvcMatchers("/api/users/**").permitAll() .mvcMatchers("/**").authenticated() ) .httpBasic(); } } |
So our application has basically mixed endpoints: unprotected and protected endpoints using Basic Auth. Fiddling around with authentication is now the last thing we want to do when testing.
Let's use the following API endpoint as an example:
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 | @RestController @RequestMapping("/api/tasks") public class TaskController { private final TaskService taskService; public TaskController(TaskService taskService) { this.taskService = taskService; } @PostMapping public ResponseEntity<Void> createNewTask(@RequestBody JsonNode payload, UriComponentsBuilder uriComponentsBuilder) { Long taskId = this.taskService.createTask(payload.get("taskTitle").asText()); return ResponseEntity .created(uriComponentsBuilder.path("/api/tasks/{taskId}").build(taskId)) .build(); } @DeleteMapping @RolesAllowed("ADMIN") @RequestMapping("/{taskId}") public void deleteTask(@PathVariable Long taskId) { this.taskService.deleteTask(taskId); } } |
Valid test scenarios would now include verifying that we block anonymous users, allow authenticated users access, and only allow privileged users to delete tasks.
The anonymous part is simple. Just invoke the endpoint without any further setup and expect HTTP status 401:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @WebMvcTest(TaskController.class) class TaskControllerTest { @Autowired private MockMvc mockMvc; @MockBean private TaskService taskService; @Test public void shouldRejectCreatingReviewsWhenUserIsAnonymous() throws Exception { this.mockMvc .perform( post("/api/tasks") .contentType(MediaType.APPLICATION_JSON) .content("{\"taskTitle\": \"Learn MockMvc\"}") .with(csrf()) ) .andExpect(status().isUnauthorized()); } } |
When we now want to test the happy-path of creating a task, we need an authenticated user accessing the endpoint. Including the Spring Security Test dependency (see the first section), we have multiple ways to inject a user to the Security Context of the mocked Servlet environment.
The most simple one is user()
, where we can specify any user-related attributes alongside the MockMvc
request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Test public void shouldReturnLocationOfReviewWhenUserIsAuthenticatedAndCreatesReview() throws Exception { when(taskService.createTask(anyString())).thenReturn(42L); this.mockMvc .perform( post("/api/tasks") .contentType(MediaType.APPLICATION_JSON) .content("{\"taskTitle\": \"Learn MockMvc\"}") .with(csrf()) .with(SecurityMockMvcRequestPostProcessors.user("duke")) ) .andExpect(status().isCreated()) .andExpect(header().exists("Location")) .andExpect(header().string("Location", Matchers.containsString("42"))); } |
This will establish a SecurityContext
during our test that includes the user duke.
We can further tweak the user setup and assign roles to test the DELETE endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 | @Test public void shouldAllowDeletingReviewsWhenUserIsAdmin() throws Exception { this.mockMvc .perform( delete("/api/tasks/42") .with(user("duke").roles("ADMIN", "SUPER_USER")) .with(csrf()) ) .andExpect(status().isOk()); verify(taskService).deleteTask(42L); } |
There are way more SecurityMockMvcRequestPostProcessors
available, that allow setting up users for your authentication method: jwt()
, oauth2Login()
, digest()
, opaqueToken()
, etc.
You can also use @WithMockUser
on top of your test to define the mocked user for the whole test execution:
1 2 3 4 5 6 7 | @Test @WithMockUser("duke") public void shouldRejectDeletingReviewsWhenUserLacksAdminRole() throws Exception { this.mockMvc .perform(delete("/api/tasks/42")) .andExpect(status().isForbidden()); } |
MockMvc in combination with @SpringBootTest
You can also use MockMvc
together with @SpringBootTest
. With this setup, you'll get your whole Spring application context populated and don't have to mock any service class. Such tests ensure the integration of multiple parts of your application (aka. integration tests):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @SpringBootTest @AutoConfigureMockMvc class ApplicationTests { @Autowired private MockMvc mockMvc; @Test public void shouldAllowDeletingReviewsWhenUserIsAdmin() throws Exception { this.mockMvc .perform( delete("/api/tasks/42") .with(SecurityMockMvcRequestPostProcessors.user("duke").roles("ADMIN", "SUPER_USER")) .with(csrf()) ) .andExpect(status().isOk()); } } |
Also, make sure to have at least some happy-path tests that use HTTP and not a mocked servlet environment. Therefore you can make use of the WebTestClient for example.
You can find the demo application for this testing Spring Boot MVC controllers example using MockMvc on GitHub.
PS: Do you want more tips & tricks about testing Spring Boot applications? Make sure to join the Testing Spring Boot Applications Masterclass where we'll also tackle Testing Spring Boot endpoints with MockMvc
in detail.
Joyful testing your Spring Boot application with MockMvc,
Philip
[…] 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 […]
[…] testing this controller, we can use @WebMvcTest to test the controller with MockMvc. In addition to this, we get a sliced Spring Context that contains only relevant Spring Beans for […]
[…] >> Guide to Testing Spring Boot applications with MockMvc [rieckpil.de] […]