A Practical Guide to Testing Spring Controllers With MockMvcTester

Spring Framework 6.2 introduced MockMvcTester to support writing AssertJ style assertions using AssertJ under the hood. If you’re using Spring Boot, the spring-boot-starter-test dependency transitively adds the most commonly used testing libraries such as mockito, assertj, json-path, jsonassert, etc. So, if you’re using Spring Boot 3.4.0 (which uses Spring framework 6.2) or any later version, […]

Apr 22, 2025 - 16:46
 0
A Practical Guide to Testing Spring Controllers With MockMvcTester

Spring Framework 6.2 introduced MockMvcTester to support writing AssertJ style assertions using AssertJ under the hood.

If you’re using Spring Boot, the spring-boot-starter-test dependency transitively adds the most commonly used testing libraries such as mockito, assertj, json-path, jsonassert, etc. So, if you’re using Spring Boot 3.4.0 (which uses Spring framework 6.2) or any later version, you don’t need to add any extra dependencies to use MockMvcTester.

In this article, we’ll explore how you can use MockMvcTester for different testing scenarios.

Getting started with MockMvcTester

MockMvcTester is built on top of MockMvc and provides AssertJ support for writing tests and asserting the result.

You can find the sample application code here.

Here is an example test written using MockMvc:

import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class UserRestControllerTests {
   @Autowired
   MockMvc mockMvc;

   @Test
   void getUserByIdSuccessful() throws Exception {
       mockMvc.perform(get("/api/users/1")).andExpect(status().isOk());
   }
}

The same test written using the MockMvcTester fluent API:

import org.springframework.test.web.servlet.assertj.MockMvcTester;
import static org.assertj.core.api.Assertions.assertThat;

class UserRestControllerTests {
   @Autowired
   MockMvcTester mockMvcTester;

   @Test
   void getUserByIdSuccessful() {
      assertThat(mockMvcTester.get().uri("/api/users/1")).hasStatusOk();
   }
}

It is much easier to use the MockMvcTester fluent API rather than using the IDE feature to find the static imports for get(), post(), status(), etc.

How to configure MockMvcTester?

If you’re writing a slice test for a controller using @WebMvcTest, you can simply inject MockMvcTester.

The @WebMvcTest annotation is meta-annotated with @AutoConfigureMockMvc, so a MockMvc instance is auto-configured. If AssertJ is available, then a MockMvcTester instance will also be auto-configured.

@WebMvcTest(controllers = UserRestController.class)
class UserRestControllerTests {
   @Autowired
   MockMvcTester mockMvcTester;

   //...
}

If you’re writing an integration test using @SpringBootTest, then you need to add the @AutoConfigureMockMvc annotation to the test class, and you can also inject MockMvcTester.

@SpringBootTest
@AutoConfigureMockMvc
class UserRestControllerTests {
   @Autowired
   MockMvcTester mockMvcTester;

   //...
}

If you’re already using MockMvc, then you can gradually adopt MockMvcTester by creating a MockMvcTester instance from MockMvc:

class UserRestControllerTests {
   @Autowired
   MockMvc mockMvc;

   MockMvcTester mockMvcTester;

   @PostConstruct
   void setUp() {
       mockMvcTester = MockMvcTester.create(mockMvc);
   }

   //...
}

Writing tests using MockMvcTester

Let’s explore how we can write tests using MockMvcTester in various scenarios.

Testing REST API JSON response

Assume we have a REST API endpoint for user registration that returns HTTP code 201 when successful, along with the response JSON payload with name, email, and role properties.

We can write a test using MockMvcTester:

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult;

import static org.assertj.core.api.Assertions.assertThat;

@Test
void userRegistrationSuccessful() {
   String requestBody = """
           {
               "email": "siva@gmail.com",
               "password": "secret",
               "name": "Siva"
           }
           """;

   assertThat(mockMvcTester
       	.post()
       	.uri("/api/users")
       	.contentType(MediaType.APPLICATION_JSON)
       	.content(requestBody))
        .hasStatus(HttpStatus.CREATED)
        .bodyJson()
        .isLenientlyEqualTo("""
             {
               "name": "Siva",
               "email": "siva@gmail.com",
               "role": "ROLE_USER"
             }
          """);
}

We have compared the HTTP status code, converted the response body to JSON, and compared it with our expected JSON structure.

We can split the code into two parts to execute the request and assert the result:

MvcTestResult testResult = mockMvcTester
       .post()
       .uri("/api/users")
       .contentType(MediaType.APPLICATION_JSON)
       .content(requestBody)
       .exchange();

assertThat(testResult)
       .hasStatus(HttpStatus.CREATED)
       .bodyJson()
       .isLenientlyEqualTo("""
           {
              "name": "Siva",
              "email": "siva@gmail.com",
              "role": "ROLE_USER"
           }
         """);

So far, we have compared the response JSON with our expected JSON structure using a multiline string. Instead, we can also store the JSON as a classpath resource and compare them:

var expected = new ClassPathResource("/user-registration-response.json", UserRestControllerTests.class);

assertThat(testResult)
       .hasStatus(HttpStatus.CREATED)
       .bodyJson()
       .isLenientlyEqualTo(expected);

If you need more control over the response body assertions, you can map the response into a Java object and assert it:

public record RegistrationResponse(String name, String email, String role) {}

assertThat(testResult)
       .hasStatus(HttpStatus.CREATED)
       .bodyJson()
       .convertTo(RegistrationResponse.class)
       .satisfies(response -> {
           assertThat(response.name()).isEqualTo("Siva");
           assertThat(response.email()).isEqualTo("siva@gmail.com");
           assertThat(response.role()).isEqualTo("ROLE_USER");
       });

Testing REST API exception handling scenarios

It is common practice to use @RestControllerAdvice to handle exceptions centrally:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

   @ExceptionHandler(UserAlreadyExistsException.class)
   public ResponseEntity handle(UserAlreadyExistsException e) {
       var error = e.getMessage();
       return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
   }

   // more handler methods
}



For user registration, we check the existence of the given email in the database and throw UserAlreadyExistsException if the email already exists. The GlobalExceptionHandler will handle this exception and return the appropriate response.

We can write a test to handle this scenario using MockMvcTester:

@Test
void shouldFailToRegisterWithExistingEmail() {
   String requestBody = """
           {
               "email": "admin@gmail.com",
               "password": "secret",
               "name": "Administrator"
           }
           """;

   MvcTestResult testResult = mockMvcTester.post()
           .uri("/api/users")
           .contentType(MediaType.APPLICATION_JSON)
           .content(requestBody)
           .exchange();

   assertThat(testResult)
           .failure()
           .isInstanceOf(UserAlreadyExistsException.class)
           .hasMessage("User with email admin@gmail.com already exists");
}

We have asserted that there is a failure with a specific exception type and the expected error message.

Testing the Thymeleaf view rendering controllers

We can write tests for controllers that handle the request and render a view, such as a Thymeleaf view template.

Let’s say we have a controller with two handler methods:

import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class UserController {
   private final UserRepository userRepository;
   public UserController(UserRepository userRepository) {
       this.userRepository = userRepository;
   }

   @GetMapping("/users/{id}")
   public String getUserById(@PathVariable Long id, Model model) {
       var user = userRepository.findById(id);
       if (user != null) {
           model.addAttribute("user", user);
           return "user";
       }
       return "not-found";
   }

   @PostMapping("/users")
   public String createUser(@ModelAttribute("user") @Valid User user,
                            BindingResult bindingResult,
                            RedirectAttributes redirectAttributes) {
       userRepository.create(user);
       if (bindingResult.hasErrors()) {
           return "create-user";
       }
       redirectAttributes.addFlashAttribute("successMessage", "User saved successfully");
       return "redirect:/users";
   }
}

Let’s write the first test to invoke GET users/{id} and assert the HTTP status code and the model data:

@Test
void shouldGetUserById() {
   var result = mockMvcTester.get().uri("/users/1").exchange();

   assertThat(result)
           .hasStatusOk()
           .hasViewName("user")
           .model()
           	.containsKeys("user")
           	.containsEntry("user", new User(1L, "Siva", "siva@gmail.com", "siva"));
}

Here, we assert the expected view name, model attribute name to be user, and the user object data.

Testing URL redirects and flash attributes

Let’s write a test to verify the successful scenario of creating a user with valid data:

@Test
void shouldCreateUserSuccessfully() {
   var result = mockMvcTester.post().uri("/users")
           .contentType(MediaType.APPLICATION_FORM_URLENCODED)
           .param("name", "Test User 4")
           .param("email", "testuser4@gmail.com")
           .param("password", "testuser4")
           .exchange();

   assertThat(result)
           .hasStatus(HttpStatus.FOUND)
           .hasRedirectedUrl("/users")
           .flash().containsKey("successMessage")
           .hasEntrySatisfying("successMessage",
                   value -> assertThat(value).isEqualTo("User saved successfully"));
}

We have submitted the form with valid data and asserted the expected behavior that the user will be redirected to the new URL /users with a successMessage flash attribute.

Testing model validation errors

When a form is submitted with invalid data, we usually redisplay the form with error messages.

Let’s see how we can test the form field validation errors:

@Test
void shouldGetErrorsWhenUserDataIsInvalid() {
   var result = mockMvcTester.post().uri("/users")
           .contentType(MediaType.APPLICATION_FORM_URLENCODED)
           .param("name", "") // blank -invalid
           .param("email", "testuser4gmail.com") // invalid email format
           .param("password", "pwd") // valid
           .exchange();

   assertThat(result)
           .model()
           .extractingBindingResult("user")
           .hasErrorsCount(2)
           .hasFieldErrors("name", "email");
}

Here, we have submitted the form with invalid values for the name and email fields and asserted the expected error details.

Similarly, you can assert the expected headers, cookies, multipart requests, etc.

Summary

As you have seen in this article, MockMvcTester helps you to write tests using a fluent API and provides many custom assertions to verify the results more expressively.

To learn more about MockMvcTester, you can check out the official documentation here.

If you’re using Spring Boot 3.4.0 or a later version, you can start using MockMvcTester to write more expressive tests using a fluent API. If you’re using an older version, then MockMvcTester could be a solid reason to consider upgrading!

This site uses cookies. By continuing to browse the site you are agreeing to our use of cookies.