Spring MockMVC
Introduction
When developing web applications with Spring MVC, testing your controllers is essential to ensure they behave as expected. However, testing controllers traditionally requires deploying your application to a server, which can be slow and cumbersome during development. This is where Spring's MockMVC comes in.
Spring MockMVC is a powerful testing framework that allows you to test your Spring MVC controllers without having to deploy them to a server. It simulates HTTP requests and responses, making it possible to test your controllers' behavior in isolation, without starting an actual HTTP server.
In this guide, you'll learn:
- What MockMVC is and why it's useful
- How to set up MockMVC in your project
- How to write basic controller tests
- Advanced techniques for testing complex scenarios
Prerequisites
Before diving into MockMVC, make sure you have:
- Basic knowledge of Spring Framework
- Understanding of Spring MVC concepts
- Familiarity with JUnit or other testing frameworks
- A Spring Boot project set up with Spring MVC
Setting Up MockMVC
Dependencies
First, ensure you have the necessary dependencies in your project. If you're using Spring Boot, these dependencies are typically included in spring-boot-starter-test
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
For Gradle:
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Creating a MockMVC Instance
There are two main ways to set up MockMVC:
1. Standalone Setup
Standalone setup is used when you want to test controllers in isolation without loading the full Spring context:
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
@ExtendWith(SpringExtension.class)
public class UserControllerTest {
private MockMvc mockMvc;
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
mockMvc = standaloneSetup(userController).build();
}
// Test methods...
}
2. Web Application Context Setup
This approach loads a complete application context, providing a more realistic test environment:
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
// Test methods...
}
With @AutoConfigureMockMvc
, Spring Boot automatically configures the MockMvc instance for you.
Writing Basic MockMVC Tests
Now that you have MockMVC set up, let's write some tests for a simple user controller:
First, let's look at our controller:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody @Valid User user) {
User savedUser = userService.save(user);
return ResponseEntity
.created(URI.create("/api/users/" + savedUser.getId()))
.body(savedUser);
}
}
Testing GET Requests
Here's how to test a GET request:
@Test
public void shouldReturnUserWhenUserExists() throws Exception {
// Given
User user = new User(1L, "John", "[email protected]");
when(userService.findById(1L)).thenReturn(Optional.of(user));
// When & Then
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
@Test
public void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
// Given
when(userService.findById(99L)).thenReturn(Optional.empty());
// When & Then
mockMvc.perform(get("/api/users/99")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
Testing POST Requests
Here's how to test a POST request with a request body:
@Test
public void shouldCreateUserSuccessfully() throws Exception {
// Given
User userToCreate = new User(null, "Sarah", "[email protected]");
User savedUser = new User(1L, "Sarah", "[email protected]");
when(userService.save(any(User.class))).thenReturn(savedUser);
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Sarah\",\"email\":\"[email protected]\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/users/1"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Sarah"))
.andExpect(jsonPath("$.email").value("[email protected]"));
// Verify that service was called
verify(userService).save(any(User.class));
}
Advanced MockMVC Techniques
Testing with Path Variables and Request Parameters
@Test
public void shouldFilterUsersByName() throws Exception {
// Given
List<User> matchingUsers = Arrays.asList(
new User(1L, "John Smith", "[email protected]"),
new User(2L, "Johnny Walker", "[email protected]")
);
when(userService.findByNameContaining("John")).thenReturn(matchingUsers);
// When & Then
mockMvc.perform(get("/api/users")
.param("name", "John")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("John Smith"))
.andExpect(jsonPath("$[1].name").value("Johnny Walker"));
}
Testing Exception Handling
Let's say our controller has exception handling for validation errors:
@Test
public void shouldReturn400WhenEmailIsInvalid() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Sarah\",\"email\":\"invalid-email\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors[*]", hasItem(containsString("valid email"))));
}
Testing with Authentication
To test endpoints that require authentication:
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
public void shouldAllowAdminToDeleteUser() throws Exception {
mockMvc.perform(delete("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNoContent());
verify(userService).deleteById(1L);
}
@Test
@WithMockUser(username = "user", roles = {"USER"})
public void shouldForbidRegularUserFromDeletingUser() throws Exception {
mockMvc.perform(delete("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isForbidden());
verify(userService, never()).deleteById(anyLong());
}
Debugging MockMVC Tests
When tests fail, it can be helpful to see exactly what was returned. You can use print()
to output the response to the console:
mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andDo(print()) // Prints request and response details
.andExpect(status().isOk());
You can also log the JSON response for closer inspection:
MvcResult result = mockMvc.perform(get("/api/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andReturn();
String responseContent = result.getResponse().getContentAsString();
System.out.println("Response: " + responseContent);
Real-World Example: Testing a Complete REST API
Let's build a more comprehensive test for a REST API that manages blog posts:
@SpringBootTest
@AutoConfigureMockMvc
public class BlogPostControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private BlogPostService blogPostService;
@Test
public void shouldReturnBlogPostsWithPagination() throws Exception {
// Given
List<BlogPost> posts = List.of(
new BlogPost(1L, "Spring Testing", "A guide to testing Spring applications", "john", LocalDateTime.now()),
new BlogPost(2L, "MockMVC Tutorial", "Learn how to use MockMVC", "sarah", LocalDateTime.now())
);
Page<BlogPost> page = new PageImpl<>(posts, PageRequest.of(0, 10), 2);
when(blogPostService.findAll(any(Pageable.class))).thenReturn(page);
// When & Then
mockMvc.perform(get("/api/posts")
.param("page", "0")
.param("size", "10")
.param("sort", "createdAt,desc")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content", hasSize(2)))
.andExpect(jsonPath("$.content[0].title").value("Spring Testing"))
.andExpect(jsonPath("$.content[1].title").value("MockMVC Tutorial"))
.andExpect(jsonPath("$.totalElements").value(2))
.andExpect(jsonPath("$.totalPages").value(1))
.andExpect(jsonPath("$.size").value(10))
.andExpect(jsonPath("$.number").value(0));
}
@Test
@WithMockUser(roles = "EDITOR")
public void shouldCreateAndReturnNewBlogPost() throws Exception {
// Given
BlogPost newPost = new BlogPost(null, "Testing with MockMVC", "Content here", "editor", null);
BlogPost savedPost = new BlogPost(3L, "Testing with MockMVC", "Content here", "editor", LocalDateTime.now());
when(blogPostService.create(any(BlogPost.class))).thenReturn(savedPost);
// When & Then
mockMvc.perform(post("/api/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"Testing with MockMVC\",\"content\":\"Content here\"}"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/posts/3")))
.andExpect(jsonPath("$.id").value(3))
.andExpect(jsonPath("$.title").value("Testing with MockMVC"))
.andExpect(jsonPath("$.author").value("editor"));
ArgumentCaptor<BlogPost> postCaptor = ArgumentCaptor.forClass(BlogPost.class);
verify(blogPostService).create(postCaptor.capture());
assertEquals("Testing with MockMVC", postCaptor.getValue().getTitle());
}
}
Best Practices for MockMVC Testing
- Test in Isolation: Use standalone setup for unit tests to test controllers in isolation.
- Use WebApplicationContext for Integration Tests: For integration tests, use the web application context setup to test with the full Spring context.
- Test Both Happy Path and Error Cases: Ensure you test both successful scenarios and error conditions.
- Verify Service Interactions: Use Mockito to verify that your controllers interact correctly with services.
- Test Security Constraints: If your API has security constraints, test with and without the required permissions.
- Use Appropriate Matchers: Utilize JSONPath expressions and Hamcrest matchers to validate response content accurately.
- Keep Tests Readable: Use descriptive test names and comments to make tests easy to understand.
- Test Complex Request Bodies: For controllers that accept complex JSON structures, test with various input combinations.
Common MockMVC Assertions
Here's a quick reference for commonly used MockMVC assertions:
// Status code assertions
.andExpect(status().isOk()) // 200
.andExpect(status().isCreated()) // 201
.andExpect(status().isNoContent()) // 204
.andExpect(status().isBadRequest()) // 400
.andExpect(status().isUnauthorized()) // 401
.andExpect(status().isForbidden()) // 403
.andExpect(status().isNotFound()) // 404
// Header assertions
.andExpect(header().string("Location", "/api/users/1"))
.andExpect(header().exists("ETag"))
.andExpect(header().doesNotExist("X-Custom-Header"))
// Content assertions
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(content().json("{'name':'John', 'age':30}"))
.andExpect(content().string("Expected response string"))
// JSON path assertions
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.age").value(30))
.andExpect(jsonPath("$.addresses").isArray())
.andExpect(jsonPath("$.addresses", hasSize(2)))
.andExpect(jsonPath("$.addresses[0].city").value("New York"))
Summary
Spring MockMVC is an invaluable tool for testing Spring MVC controllers without deploying your application to a server. It allows you to:
- Simulate HTTP requests with different HTTP methods, headers, path variables, and request parameters
- Assert various aspects of HTTP responses like status codes, headers, and response bodies
- Test your controllers in isolation or within the Spring context
- Validate both happy paths and error scenarios
- Test security constraints and authentication requirements
By incorporating MockMVC into your testing strategy, you can ensure that your controllers behave as expected, leading to more reliable and robust Spring web applications.
Additional Resources
Exercises
- Write MockMVC tests for a controller that handles CRUD operations for a Product entity.
- Create tests for a controller that requires authentication and has different endpoints for different user roles.
- Test a controller that handles file uploads using MockMVC.
- Write tests for a controller with custom exception handling for various business rule violations.
- Create a test suite that validates pagination, sorting, and filtering for a REST API endpoint.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)