If you've ever wanted to see your API living its best life but don't want to boot the whole server and fuss with Postman, MockMvc to the rescue. Today we'll dive into practical use of MockMvc to test controllers in your Spring app.
MockMvc is a tool for testing Spring MVC. It lets you run HTTP requests against your controllers without having to start a real server. In other words, MockMvc is your pocket Postman — but way more integrated and automation-friendly. With MockMvc you can easily mock requests, verify controller responses, test error scenarios, and assert HTTP statuses.
Setting up the environment
Before you start testing with MockMvc, make sure your app is set up for tests. That means adding dependencies to your build file (for example, pom.xml if you're using Maven):
<dependencies>
<!-- Dependency for Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Example controller
For testing we'll use a REST controller handling the User entity. Here's what it might look like:
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable("id") Long id) {
// Assume a service is called here that returns the user
User user = new User(id, "John Doe");
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
// Simulate creating a user
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable("id") Long id) {
// Assume the user was deleted successfully
return ResponseEntity.noContent().build();
}
}
Hands-on: testing the controller with MockMvc
Now let's test UserController. We'll cover the main methods: getting, creating, and deleting a user.
Setting up MockMvc in tests
In Spring Boot you can use the @WebMvcTest annotation to spin up a context just for web components like controllers.
@WebMvcTest(UserController.class) // Spin up the context only for UserController
class UserControllerTest {
@Autowired
private MockMvc mockMvc; // MockMvc lets you send requests.
@Test
void testGetUserById() throws Exception {
// Test logic goes here
}
}
Testing a GET request
Let's start with the basics: testing that the controller correctly responds to a get user request.
@Test
void testGetUserById() throws Exception {
mockMvc.perform(get("/api/users/{id}", 1)) // Send a GET request to the endpoint.
.andExpect(status().isOk()) // Check that the response status is 200 OK.
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) // Check the content type.
.andExpect(jsonPath("$.id").value(1)) // Check that the "id" field equals 1.
.andExpect(jsonPath("$.name").value("John Doe")); // Check that the "name" field equals "John Doe".
}
Here we use jsonPath to inspect the JSON response body. It's super handy and even works with nested objects.
Testing POST requests
Now let's add a test for the create user method.
@Test
void testGetUserById() throws Exception {
mockMvc.perform(get("/api/users/{id}", 1)) // Send a GET request to the endpoint.
.andExpect(status().isOk()) // Check that the response status is 200 OK.
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) // Check the content type.
.andExpect(jsonPath("$.id").value(1)) // Check that the "id" field equals 1.
.andExpect(jsonPath("$.name").value("John Doe")); // Check that the "name" field equals "John Doe".
}
Here you can see how easy it is to pass a request body as JSON. We also check for a Location header, which is often used in responses to POST requests.
Testing DELETE requests
Let's test deleting a user. It's important to make sure the controller returns 204 No Content.
@Test
void testDeleteUser() throws Exception {
mockMvc.perform(delete("/api/users/{id}", 1)) // Send a DELETE request.
.andExpect(status().isNoContent()); // Check that the response status is 204 No Content.
}
Testing negative scenarios
Your endpoints don't always respond successfully. Sometimes they throw errors, and it's important to test those scenarios too.
Suppose that the getUserById method can throw an exception if the user doesn't exist:
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable("id") Long id) {
if (id < 0) {
throw new UserNotFoundException("User not found");
}
User user = new User(id, "John Doe");
return ResponseEntity.ok(user);
}
Here's an example test for that scenario:
@Test
void testGetUserNotFound() throws Exception {
mockMvc.perform(get("/api/users/{id}", -1)) // Request with an invalid ID.
.andExpect(status().isNotFound()) // Expect a 404 Not Found status.
.andExpect(jsonPath("$.error").value("User not found")); // Check the error message.
}
Common mistakes and how to fix them
Many beginners run into issues when using MockMvc. For example, they forget to use @WebMvcTest, and then the test starts pulling in dependencies on the whole database, services, and other app components. Don't forget to configure MockMvc and isolate the layer you're testing.
Another common mistake is missing request parameters. For example, if you forget to pass a request body in POST, MockMvc will return a 400 error. Always double-check what your endpoint expects.
What's next?
Testing with MockMvc is just one step in the quality chain. In real projects you'll combine unit, integration, and functional tests to minimize bugs and maximize peace of mind. Later in the course we'll dive into integration testing with MockMvc and hooking up real databases via Testcontainers.
For a deeper dive, check out the official MockMvc documentation.
GO TO FULL VERSION