{"id":557045,"date":"2025-04-22T14:22:39","date_gmt":"2025-04-22T13:22:39","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=idea&#038;p=557045"},"modified":"2025-04-22T14:28:40","modified_gmt":"2025-04-22T13:28:40","slug":"a-practical-guide-to-testing-spring-controllers-with-mockmvctester","status":"publish","type":"idea","link":"https:\/\/blog.jetbrains.com\/zh-hans\/idea\/2025\/04\/a-practical-guide-to-testing-spring-controllers-with-mockmvctester","title":{"rendered":"A Practical Guide to Testing Spring\u00a0Controllers With MockMvcTester"},"content":{"rendered":"\n<p>Spring Framework 6.2 introduced <code>MockMvcTester<\/code> to support writing <a href=\"https:\/\/assertj.github.io\/doc\/\" target=\"_blank\" rel=\"noopener\">AssertJ<\/a> style assertions using AssertJ under the hood.<\/p>\n\n\n\n<p>If you\u2019re using Spring Boot, the <code>spring-boot-starter-test<\/code> dependency transitively adds the most commonly used testing libraries such as <code>mockito<\/code>, <code>assertj<\/code>, <code>json-path<\/code>, <code>jsonassert<\/code>, etc. So, if you\u2019re using Spring Boot 3.4.0 (which uses Spring framework 6.2) or any later version, you don\u2019t need to add any extra dependencies to use <code>MockMvcTester<\/code>.<\/p>\n\n\n\n<p>In this article, we\u2019ll explore how you can use <code>MockMvcTester<\/code> for different testing scenarios.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Getting started with MockMvcTester<\/h2>\n\n\n\n<p><code>MockMvcTester<\/code> is built on top of <code>MockMvc<\/code> and provides AssertJ support for writing tests and asserting the result.<\/p>\n\n\n\n<p><strong>You can find the sample application code <\/strong><a href=\"https:\/\/github.com\/sivaprasadreddy\/spring-mockmvctester-demo\" target=\"_blank\" rel=\"noopener\"><strong>here<\/strong><\/a><strong>.<\/strong><\/p>\n\n\n\n<p>Here is an example test written using <code>MockMvc<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import org.springframework.test.web.servlet.MockMvc;\nimport static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;\n\nclass UserRestControllerTests {\n   @Autowired\n   MockMvc mockMvc;\n\n   @Test\n   void getUserByIdSuccessful() throws Exception {\n       mockMvc.perform(get(\"\/api\/users\/1\")).andExpect(status().isOk());\n   }\n}<\/pre>\n\n\n\n<p>The same test written using the <code>MockMvcTester<\/code> fluent API:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import org.springframework.test.web.servlet.assertj.MockMvcTester;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nclass UserRestControllerTests {\n   @Autowired\n   MockMvcTester mockMvcTester;\n\n   @Test\n   void getUserByIdSuccessful() {\n      assertThat(mockMvcTester.get().uri(\"\/api\/users\/1\")).hasStatusOk();\n   }\n}\n<\/pre>\n\n\n\n<p>It is much easier to use the <code>MockMvcTester<\/code> fluent API rather than using the IDE feature to find the static imports for <code>get()<\/code>, <code>post()<\/code>, <code>status()<\/code>, etc.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How to configure MockMvcTester?<\/h3>\n\n\n\n<p>If you\u2019re writing a slice test for a controller using <code>@WebMvcTest<\/code>, you can simply inject <code>MockMvcTester<\/code>.<\/p>\n\n\n\n<p>The <code>@WebMvcTest<\/code> annotation is meta-annotated with <code>@AutoConfigureMockMvc<\/code>, so a <code>MockMvc<\/code> instance is auto-configured. If AssertJ is available, then a <code>MockMvcTester<\/code> instance will also be auto-configured.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@WebMvcTest(controllers = UserRestController.class)\nclass UserRestControllerTests {\n   @Autowired\n   MockMvcTester mockMvcTester;\n\n   \/\/...\n}<\/pre>\n\n\n\n<p>If you\u2019re writing an integration test using <code>@SpringBootTest<\/code>, then you need to add the <code>@AutoConfigureMockMvc<\/code> annotation to the test class, and you can also inject <code>MockMvcTester<\/code>.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@SpringBootTest\n@AutoConfigureMockMvc\nclass UserRestControllerTests {\n   @Autowired\n   MockMvcTester mockMvcTester;\n\n   \/\/...\n}<\/pre>\n\n\n\n<p>If you\u2019re already using <code>MockMvc<\/code>, then you can gradually adopt <code>MockMvcTester<\/code> by creating a <code>MockMvcTester<\/code> instance from <code>MockMvc<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">class UserRestControllerTests {\n   @Autowired\n   MockMvc mockMvc;\n\n   MockMvcTester mockMvcTester;\n\n   @PostConstruct\n   void setUp() {\n       mockMvcTester = MockMvcTester.create(mockMvc);\n   }\n\n   \/\/...\n}<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Writing tests using MockMvcTester<\/h2>\n\n\n\n<p>Let\u2019s explore how we can write tests using <code>MockMvcTester<\/code> in various scenarios.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Testing REST API JSON response<\/h3>\n\n\n\n<p>Assume we have a REST API endpoint for user registration that returns HTTP code 201 when successful, along with the response JSON payload with <code>name<\/code>, <code>email<\/code>, and <code>role<\/code> properties.<\/p>\n\n\n\n<p>We can write a test using <code>MockMvcTester<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.servlet.assertj.MockMvcTester;\nimport org.springframework.test.web.servlet.assertj.MvcTestResult;\n\nimport static org.assertj.core.api.Assertions.assertThat;\n\n@Test\nvoid userRegistrationSuccessful() {\n   String requestBody = \"\"\"\n           {\n               \"email\": \"siva@gmail.com\",\n               \"password\": \"secret\",\n               \"name\": \"Siva\"\n           }\n           \"\"\";\n\n   assertThat(mockMvcTester\n       \t.post()\n       \t.uri(\"\/api\/users\")\n       \t.contentType(MediaType.APPLICATION_JSON)\n       \t.content(requestBody))\n        .hasStatus(HttpStatus.CREATED)\n        .bodyJson()\n        .isLenientlyEqualTo(\"\"\"\n             {\n               \"name\": \"Siva\",\n               \"email\": \"siva@gmail.com\",\n               \"role\": \"ROLE_USER\"\n             }\n          \"\"\");\n}<\/pre>\n\n\n\n<p>We have compared the HTTP status code, converted the response body to JSON, and compared it with our expected JSON structure.<\/p>\n\n\n\n<p>We can split the code into two parts to execute the request and assert the result:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">MvcTestResult testResult = mockMvcTester\n       .post()\n       .uri(\"\/api\/users\")\n       .contentType(MediaType.APPLICATION_JSON)\n       .content(requestBody)\n       .exchange();\n\nassertThat(testResult)\n       .hasStatus(HttpStatus.CREATED)\n       .bodyJson()\n       .isLenientlyEqualTo(\"\"\"\n           {\n              \"name\": \"Siva\",\n              \"email\": \"siva@gmail.com\",\n              \"role\": \"ROLE_USER\"\n           }\n         \"\"\");<\/pre>\n\n\n\n<p>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:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">var expected = new ClassPathResource(\"\/user-registration-response.json\", UserRestControllerTests.class);\n\nassertThat(testResult)\n       .hasStatus(HttpStatus.CREATED)\n       .bodyJson()\n       .isLenientlyEqualTo(expected);<\/pre>\n\n\n\n<p>If you need more control over the response body assertions, you can map the response into a Java object and assert it:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public record RegistrationResponse(String name, String email, String role) {}\n\nassertThat(testResult)\n       .hasStatus(HttpStatus.CREATED)\n       .bodyJson()\n       .convertTo(RegistrationResponse.class)\n       .satisfies(response -> {\n           assertThat(response.name()).isEqualTo(\"Siva\");\n           assertThat(response.email()).isEqualTo(\"siva@gmail.com\");\n           assertThat(response.role()).isEqualTo(\"ROLE_USER\");\n       });<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Testing REST API exception handling scenarios<\/h3>\n\n\n\n<p>It is common practice to use <code>@RestControllerAdvice<\/code> to handle exceptions centrally:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n@RestControllerAdvice\npublic class GlobalExceptionHandler {\n\n   @ExceptionHandler(UserAlreadyExistsException.class)\n   public ResponseEntity&lt;Object> handle(UserAlreadyExistsException e) {\n       var error = e.getMessage();\n       return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);\n   }\n\n   \/\/ more handler methods\n}<\/pre>\n\n\n\n<p>For user registration, we check the existence of the given email in the database and throw <code>UserAlreadyExistsException<\/code> if the email already exists. The <code>GlobalExceptionHandler<\/code> will handle this exception and return the appropriate response.<\/p>\n\n\n\n<p>We can write a test to handle this scenario using <code>MockMvcTester<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Test\nvoid shouldFailToRegisterWithExistingEmail() {\n   String requestBody = \"\"\"\n           {\n               \"email\": \"admin@gmail.com\",\n               \"password\": \"secret\",\n               \"name\": \"Administrator\"\n           }\n           \"\"\";\n\n   MvcTestResult testResult = mockMvcTester.post()\n           .uri(\"\/api\/users\")\n           .contentType(MediaType.APPLICATION_JSON)\n           .content(requestBody)\n           .exchange();\n\n   assertThat(testResult)\n           .failure()\n           .isInstanceOf(UserAlreadyExistsException.class)\n           .hasMessage(\"User with email admin@gmail.com already exists\");\n}<\/pre>\n\n\n\n<p>We have asserted that there is a failure with a specific exception type and the expected error message.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Testing the <em>Thymeleaf<\/em> view rendering controllers<\/h3>\n\n\n\n<p>We can write tests for controllers that handle the request and render a view, such as a Thymeleaf view template.<\/p>\n\n\n\n<p>Let\u2019s say we have a controller with two handler methods:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import jakarta.validation.Valid;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.ui.Model;\nimport org.springframework.validation.BindingResult;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.ModelAttribute;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.servlet.mvc.support.RedirectAttributes;\n\n@Controller\npublic class UserController {\n   private final UserRepository userRepository;\n   public UserController(UserRepository userRepository) {\n       this.userRepository = userRepository;\n   }\n\n   @GetMapping(\"\/users\/{id}\")\n   public String getUserById(@PathVariable Long id, Model model) {\n       var user = userRepository.findById(id);\n       if (user != null) {\n           model.addAttribute(\"user\", user);\n           return \"user\";\n       }\n       return \"not-found\";\n   }\n\n   @PostMapping(\"\/users\")\n   public String createUser(@ModelAttribute(\"user\") @Valid User user,\n                            BindingResult bindingResult,\n                            RedirectAttributes redirectAttributes) {\n       userRepository.create(user);\n       if (bindingResult.hasErrors()) {\n           return \"create-user\";\n       }\n       redirectAttributes.addFlashAttribute(\"successMessage\", \"User saved successfully\");\n       return \"redirect:\/users\";\n   }\n}<\/pre>\n\n\n\n<p>Let\u2019s write the first test to invoke <code>GET users\/{id}<\/code> and assert the HTTP status code and the model data:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Test\nvoid shouldGetUserById() {\n   var result = mockMvcTester.get().uri(\"\/users\/1\").exchange();\n\n   assertThat(result)\n           .hasStatusOk()\n           .hasViewName(\"user\")\n           .model()\n           \t.containsKeys(\"user\")\n           \t.containsEntry(\"user\", new User(1L, \"Siva\", \"siva@gmail.com\", \"siva\"));\n}<\/pre>\n\n\n\n<p>Here, we assert the expected view name, model attribute name to be <code>user<\/code>, and the user object data.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Testing URL redirects and flash attributes<\/h3>\n\n\n\n<p>Let\u2019s write a test to verify the successful scenario of creating a user with valid data:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Test\nvoid shouldCreateUserSuccessfully() {\n   var result = mockMvcTester.post().uri(\"\/users\")\n           .contentType(MediaType.APPLICATION_FORM_URLENCODED)\n           .param(\"name\", \"Test User 4\")\n           .param(\"email\", \"testuser4@gmail.com\")\n           .param(\"password\", \"testuser4\")\n           .exchange();\n\n   assertThat(result)\n           .hasStatus(HttpStatus.FOUND)\n           .hasRedirectedUrl(\"\/users\")\n           .flash().containsKey(\"successMessage\")\n           .hasEntrySatisfying(\"successMessage\",\n                   value -> assertThat(value).isEqualTo(\"User saved successfully\"));\n}<\/pre>\n\n\n\n<p>We have submitted the form with valid data and asserted the expected behavior that the user will be redirected to the new URL <code>\/users<\/code> with a <code>successMessage<\/code> flash attribute.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Testing model validation errors<\/h3>\n\n\n\n<p>When a form is submitted with invalid data, we usually redisplay the form with error messages.<\/p>\n\n\n\n<p>Let\u2019s see how we can test the form field validation errors:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Test\nvoid shouldGetErrorsWhenUserDataIsInvalid() {\n   var result = mockMvcTester.post().uri(\"\/users\")\n           .contentType(MediaType.APPLICATION_FORM_URLENCODED)\n           .param(\"name\", \"\") \/\/ blank -invalid\n           .param(\"email\", \"testuser4gmail.com\") \/\/ invalid email format\n           .param(\"password\", \"pwd\") \/\/ valid\n           .exchange();\n\n   assertThat(result)\n           .model()\n           .extractingBindingResult(\"user\")\n           .hasErrorsCount(2)\n           .hasFieldErrors(\"name\", \"email\");\n}\n<\/pre>\n\n\n\n<p>Here, we have submitted the form with invalid values for the <code>name<\/code> and <code>email<\/code> fields and asserted the expected error details.<\/p>\n\n\n\n<p>Similarly, you can assert the expected headers, cookies, multipart requests, etc.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p>As you have seen in this article, <code>MockMvcTester<\/code> helps you to write tests using a fluent API and provides many custom assertions to verify the results more expressively.<\/p>\n\n\n\n<p>To learn more about <code>MockMvcTester<\/code>, you can check out the official documentation <a href=\"https:\/\/docs.spring.io\/spring-framework\/reference\/testing\/mockmvc\/assertj.html\" target=\"_blank\" rel=\"noopener\">here<\/a>.<\/p>\n\n\n\n<p>If you\u2019re using Spring Boot 3.4.0 or a later version, you can start using <code>MockMvcTester<\/code> to write more expressive tests using a fluent API. If you\u2019re using an older version, then <code>MockMvcTester<\/code> could be a solid reason to consider upgrading!<\/p>\n","protected":false},"author":1517,"featured_media":562741,"comment_status":"closed","ping_status":"closed","template":"","categories":[4759,5088],"tags":[6603,8748,8749,276,207],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/557045"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/types\/idea"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/users\/1517"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/comments?post=557045"}],"version-history":[{"count":5,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/557045\/revisions"}],"predecessor-version":[{"id":562754,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/557045\/revisions\/562754"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media\/562741"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=557045"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/categories?post=557045"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/tags?post=557045"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/cross-post-tag?post=557045"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}