Spring Boot MockMvc Example with @WebMvcTest and MockitoBean

Test Spring Boot REST controllers with MockMvc and @WebMvcTest. Replace the service with a @MockitoBean mock and send GET, POST, PUT and DELETE requests with JSON. Then check status codes, validation errors and @RestControllerAdvice responses. Also covers MockMvcTester (AssertJ) and real andDo(print()) output on Spring Boot 4.1.1.

Spring Boot MockMvc Example

Spring MockMvc sends HTTP requests to our controllers without starting a server. Each request goes through Spring MVC’s DispatcherServlet (the class that picks the controller method for a URL), and we get the response back to check the status code and the JSON body.

We use MockMvc to test URL mapping, JSON conversion, validation and error responses of a controller in a fast test, for example to check that a request for a missing task returns 404 before the API goes to production.

The following example tests a GET endpoint of a to-do list API with @WebMvcTest. We replace the service that the controller calls with a @MockitoBean mock (a fake object that returns what we tell it to), call perform() and check the result with andExpect().

@WebMvcTest(TaskController.class)                      // loads only the web layer
class TaskControllerTest {

  @Autowired MockMvc mockMvc;
  @MockitoBean TaskService taskService;                 // a Mockito mock in place of the real bean

  @Test
  void getTaskById() throws Exception {
    given(taskService.findById(1L)).willReturn(new Task(1L, "Buy milk", false));

    mockMvc.perform(get("/tasks/{id}", 1))              // GET /tasks/1
        .andExpect(status().isOk())                     // 200
        .andExpect(jsonPath("$.title").value("Buy milk"))
        .andExpect(jsonPath("$.done").value(false));
  }
}

Notice that the test never starts Tomcat, yet the request still goes through the controller and the JSON conversion.

Since Spring Framework 6.2, MockMvcTester sends the same requests and checks them with AssertJ. For example, assertThat(mvc.get().uri(“/tasks/1”)).hasStatusOk() expects status 200, and section 2.9 shows both styles side by side.

Next, we look at how MockMvc handles a request and compare @WebMvcTest with @SpringBootTest plus @AutoConfigureMockMvc. Then we test GET, POST, PUT and DELETE endpoints with JSON bodies, and cover validation errors (400), the andDo(print()) output and exceptions handled by @RestControllerAdvice.

1. What Is MockMvc?

MockMvc is a test class from the spring-test module. When a test calls get(), post(), put() or delete(), MockMvc builds a MockHttpServletRequest, which is a request object that exists only in memory. It implements the Servlet HttpServletRequest interface, so Spring treats it like a real request.

Spring passes the request to the real DispatcherServlet, so URL mapping, JSON conversion, @Valid checks and exception handlers run the same way as in production. The response is a MockHttpServletResponse, which the test reads.

Flow diagram. The test method calls mockMvc.perform(get("/tasks/1")). A MockHttpServletRequest goes to the DispatcherServlet (filters, JSON, validation), then to TaskController. TaskController calls the TaskService mock created with @MockitoBean. The controller result, or an exception handled by ApiExceptionHandler, is written into a MockHttpServletResponse with status 200 and a JSON body. The test reads the response with andDo(print()), andExpect(status().isOk()) and jsonPath
MockMvc runs the full Spring MVC request path inside the test’s JVM. No Tomcat starts and no port opens.

No network is involved, so a MockMvc test runs fast and cannot fail because a port is taken. The other way to test a controller is to start the embedded server and send real HTTP requests over a port.

MockMvc / MockMvcTesterReal server (@SpringBootTest(webEnvironment = RANDOM_PORT))
Server startedNoYes, Tomcat on a random port
ClientMockMvc, MockMvcTesterRestTestClient, TestRestTemplate, WebTestClient
Same thread as the testYes. A @Transactional test rolls back its changes.No. The server thread commits on its own.
Tests the servlet container (TLS, real error pages)NoYes
SpeedFastSlower

For REST controllers, MockMvc covers most needs. We start a real server only when we need to test what Tomcat itself does, such as HTTP/2, compression or the container’s error page.

1.1. @WebMvcTest vs @SpringBootTest With @AutoConfigureMockMvc

Spring Boot gives us MockMvc in two kinds of tests, which differ in how many beans (objects that Spring creates and manages for us) Spring creates for the test.

Side-by-side comparison. Left, @WebMvcTest(TaskController.class). Created: TaskController, the @ControllerAdvice ApiExceptionHandler, Filter, WebMvcConfigurer and Converter beans, plus MVC, Jackson, validation and MockMvc. Not created: the @Service TaskService, which needs @MockitoBean, repositories and the DataSource. Right, @SpringBootTest with @AutoConfigureMockMvc. Every bean of the application is created, including the real TaskService. There is still no server and no port
@WebMvcTest loads only the web layer, so we call it a slice test. @SpringBootTest with @AutoConfigureMockMvc sends the request through the real services.

@WebMvcTest creates only the beans of the web layer. In practice, Spring scans @Controller, @ControllerAdvice, @JacksonComponent, Converter, Filter, HandlerInterceptor and WebMvcConfigurer beans. Spring does not create regular @Service, @Repository and @Component beans, so every object the controller depends on needs a @MockitoBean field.

We can name a controller in the annotation, as in @WebMvcTest(TaskController.class), and Spring creates only that controller. Without a name, Spring creates all controllers.

The full-context version starts the whole application, the way @SpringBootTest always does, and @AutoConfigureMockMvc adds a MockMvc bean.

@SpringBootTest
@AutoConfigureMockMvc
class TaskApiIntegrationTest {

  @Autowired MockMvc mockMvc;               // the real TaskService handles the request

  @Test
  void createThenReadTask() throws Exception {
    mockMvc.perform(post("/tasks")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""
                {"title": "Water plants", "done": false}
                """))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.id").value(1));

    mockMvc.perform(get("/tasks/{id}", 1))
        .andExpect(jsonPath("$.title").value("Water plants"));
  }
}

Most projects use both kinds, and we pick one per test class based on what the test must prove.

Question@WebMvcTest@SpringBootTest + @AutoConfigureMockMvc
What does the test check?Mapping, JSON, validation, status codes, error formatThe full request through the controller, the service and the database
Service and repository beansNot created; we add @MockitoBeanReal beans
Database neededNoYes, H2 or Testcontainers for a JPA project
Startup timeShortLonger as the application grows
When a test failsThe cause is in the web layerThe cause can be in any layer

1.2. Maven Dependencies for MockMvc in Spring Boot 4

Spring Boot 4 splits the test support into one starter per technology (a starter is one dependency that pulls in a group of libraries). For a Spring MVC application, we add spring-boot-starter-webmvc-test, which brings spring-boot-starter-test (JUnit Jupiter, Mockito, AssertJ, Hamcrest, JsonPath) and the @WebMvcTest auto-configuration, the code that sets up the web test.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc-test</artifactId>
  <scope>test</scope>
</dependency>

The example runs on Spring Boot 4.1.1 and Java 25, which give us Spring Framework 7.0.9, JUnit Jupiter 6.0.3, Mockito 5.23.0 and Jackson 3.1.5. Several imports changed in Spring Boot 4, and old imports are the usual reason a Spring Boot 3 MockMvc test stops compiling after an upgrade. Most of the moved types kept their names and changed only the package.

TypeSpring Boot 3.xSpring Boot 4.x
@WebMvcTestorg.springframework.boot.test.autoconfigure.web.servletorg.springframework.boot.webmvc.test.autoconfigure
@AutoConfigureMockMvcorg.springframework.boot.test.autoconfigure.web.servletorg.springframework.boot.webmvc.test.autoconfigure
Mock bean@MockBean (org.springframework.boot.test.mock.mockito)@MockitoBean (org.springframework.test.context.bean.override.mockito)
JSON mappercom.fasterxml.jackson.databind.ObjectMappertools.jackson.databind.json.JsonMapper
Test starterspring-boot-starter-testspring-boot-starter-webmvc-test

MockMvc, MockMvcTester and the static methods get(), status() and jsonPath() did not move, so their packages still start with org.springframework.test.web.servlet. Without Spring Boot, we need the spring-test artifact and the JUnit 5 dependencies, and we build MockMvc with MockMvcBuilders.standaloneSetup(controller).

2. Spring Boot MockMvc Example

The example is a small to-do list API, where a Task has an id, a title and a done flag. Twenty JUnit tests check every endpoint of TaskController. The tests are in the Spring-Boot-Testing repository, and mvn test runs them.

2.1. The REST Controller Under Test

TaskController has five endpoints, and each endpoint calls a TaskService method. The controller checks the request body with @Valid, and a Java record holds the task. Two Bean Validation constraints check the title. The @NotBlank constraint rejects an empty title, and @Size(max = 40) rejects a title longer than 40 characters.

public record Task(Long id,
                   @NotBlank @Size(max = 40) String title,
                   boolean done) { }

@RestController
@RequestMapping("/tasks")
public class TaskController {

  @GetMapping                 public List<Task> all() { ... }
  @GetMapping("/{id}")        public Task one(@PathVariable Long id) { ... }
  @PostMapping                public ResponseEntity<Task> create(@Valid @RequestBody Task task) { ... }   // 201 + Location
  @PutMapping("/{id}")        public Task update(@PathVariable Long id, @Valid @RequestBody Task task) { ... }
  @DeleteMapping("/{id}")     public ResponseEntity<Void> delete(@PathVariable Long id) { ... }           // 204
}

When TaskService does not find a task, it throws TaskNotFoundException. Section 2.8 shows how an exception handler turns TaskNotFoundException into a 404 response.

2.2. Replacing the Service With @MockitoBean

@MockitoBean creates a Mockito mock, which is a fake object that returns what the test tells it to return and records every call.

Spring puts the mock into the test’s context in place of the real bean of the same type, and telling a mock what to return is called stubbing. The annotation has been part of Spring Framework since 6.2, in the package org.springframework.test.context.bean.override.mockito.

@MockitoBean
TaskService taskService;

given(taskService.findById(1L)).willReturn(new Task(1L, "Buy milk", false));   // stub a return value
given(taskService.findById(99L)).willThrow(new TaskNotFoundException(99L));    // stub an exception
willThrow(new TaskNotFoundException(99L)).given(taskService).delete(99L);      // stub a void method

verify(taskService).delete(1L);                   // the controller called delete(1)
verify(taskService, never()).create(any());       // the controller never called create()

Spring Boot 4 removed @MockBean and @SpyBean, which were deprecated in Spring Boot 3.4. The replacements are @MockitoBean and @MockitoSpyBean, and a @MockitoBean field works in both @WebMvcTest and @SpringBootTest classes.

Spring Boot 2 and 3 projects still use the older @MockBean annotation. The methods given() and verify() come from Mockito.

2.3. Testing GET Endpoints and JSON Responses

A GET test passes path variables as extra arguments to get(), so get(“/tasks/{id}”, 1) calls /tasks/1. The test checks the JSON body with jsonPath(). A JsonPath expression picks one value out of the JSON document.

  • $ is the root of the document.
  • $.title is the title field.
  • $[0] is the first element of an array.
mockMvc.perform(get("/tasks/{id}", 1))
    .andExpect(status().isOk())                                      // 200
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))    // application/json
    .andExpect(jsonPath("$.id").value(1))
    .andExpect(jsonPath("$.title").value("Buy milk"));

// taskService.findAll() returns [Buy milk (open), Pay rent (done)]
mockMvc.perform(get("/tasks"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$", hasSize(2)))                            // array with 2 elements
    .andExpect(jsonPath("$[0].title").value("Buy milk"))
    .andExpect(jsonPath("$[1].done").value(true));

Query parameters and headers go on the request builder. For example, get(“/tasks”).param(“done”, “true”).header(“X-Trace”, “abc”) adds a done parameter and an X-Trace header. Spring Test has request builders and result matchers for every part of the request and the response.

2.4. Testing a POST Request With a JSON Body

A POST test sends the body with content(), and the contentType() method sets the Content-Type header. A Java text block (a string between triple quotes) keeps the JSON readable. The controller returns status 201 and a Location header that points to the new task.

given(taskService.create(any(Task.class))).willReturn(new Task(3L, "Call mom", false));

mockMvc.perform(post("/tasks")
        .contentType(MediaType.APPLICATION_JSON)
        .content("""
            {"title": "Call mom", "done": false}
            """))
    .andExpect(status().isCreated())                          // 201
    .andExpect(header().string("Location", "/tasks/3"))
    .andExpect(jsonPath("$.id").value(3));

We can also build the body from an object by injecting the JsonMapper that Spring Boot configures. In Spring Boot 4, the mapper comes from Jackson 3, so the package is tools.jackson.databind, and writeValueAsString() throws no checked exception.

@Autowired JsonMapper jsonMapper;

String body = jsonMapper.writeValueAsString(new Task(null, "Call mom", false));
// {"id":null,"title":"Call mom","done":false}

mockMvc.perform(post("/tasks").contentType(MediaType.APPLICATION_JSON).content(body))
    .andExpect(status().isCreated());

verify(taskService).create(new Task(null, "Call mom", false));   // records compare by value

The contentType(MediaType.APPLICATION_JSON) call matters, because without it Spring MVC rejects the body. Our test with MediaType.TEXT_PLAIN got 415 Unsupported Media Type.

2.5. Testing PUT and DELETE Endpoints

A PUT test looks like a POST test. The path variable goes into put(), and the new state of the task goes into the body. The calls eq(1L) and any(Task.class) are Mockito argument matchers, which describe the arguments a stub accepts, for example any Task object. If one argument uses a matcher, Mockito requires matchers for all arguments.

given(taskService.update(eq(1L), any(Task.class))).willReturn(new Task(1L, "Buy milk", true));

mockMvc.perform(put("/tasks/{id}", 1)
        .contentType(MediaType.APPLICATION_JSON)
        .content("""
            {"title": "Buy milk", "done": true}
            """))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.done").value(true));

Our DELETE endpoint returns no body, so the test checks the status and verifies that the controller called the service.

mockMvc.perform(delete("/tasks/{id}", 1))
    .andExpect(status().isNoContent());     // 204

verify(taskService).delete(1L);

By the HTTP status code rules, a create returns 201 and a delete without a body returns 204.

2.6. Reading the andDo(print()) Output

The call andDo(print()) writes the request and the response to the console, including the handler method and any resolved exception. Because every request prints about 40 lines, we add andDo(print()) while a test fails and remove the call once the test passes. For the GET test from the intro, the output shows the handler method and the JSON body.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /tasks/1
       Parameters = {}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = com.howtodoinjava.mockmvc.TaskController
           Method = com.howtodoinjava.mockmvc.TaskController#one(Long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":1,"title":"Buy milk","done":false}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Three blocks answer most “why did my test fail” questions.

  • Handler shows which controller method Spring picked, and Type = null means no mapping matched the URL and HTTP method.
  • Resolved Exception names the exception that an exception handler turned into the response. For example, a 400 from validation shows MethodArgumentNotValidException.
  • MockHttpServletResponse shows the real status, headers and body, which we compare with our jsonPath() expressions.

The call andDo(log()) sends the same text to the org.springframework.test.web.servlet.result logger at DEBUG level instead of standard output.

2.7. Testing Validation Errors (400 Bad Request)

@Valid on the @RequestBody parameter makes Spring check the @NotBlank and @Size constraints before the controller method runs. A blank title throws MethodArgumentNotValidException, so the service is never called.

Our ApiExceptionHandler handles MethodArgumentNotValidException and returns the field errors as a ProblemDetail, the RFC 9457 error format that Spring supports without extra libraries.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail invalid(MethodArgumentNotValidException ex) {
  ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
  Map<String, String> errors = new TreeMap<>();
  ex.getBindingResult().getFieldErrors()
      .forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
  problem.setProperty("errors", errors);
  return problem;
}

The test sends an empty title and checks the 400 status and the error message for the title field. Finally, verify() with never() confirms that the controller did not reach the service.

mockMvc.perform(post("/tasks")
        .contentType(MediaType.APPLICATION_JSON)
        .content("""
            {"title": "", "done": false}
            """))
    .andDo(print())
    .andExpect(status().isBadRequest())                                   // 400
    .andExpect(jsonPath("$.detail").value("Validation failed"))
    .andExpect(jsonPath("$.errors.title").value("must not be blank"));

verify(taskService, never()).create(any());
Resolved Exception:
             Type = org.springframework.web.bind.MethodArgumentNotValidException
...
MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = [Content-Type:"application/problem+json"]
     Content type = application/problem+json
             Body = {"detail":"Validation failed","instance":"/tasks","status":400,"title":"Bad Request","errors":{"title":"must not be blank"}}

Without our own handler, the 400 response has an empty body. We tested the missing-handler case by excluding ApiExceptionHandler from the test context with @WebMvcTest(excludeFilters = …). The response had Status = 400, Error message = Invalid request content. and an empty Body =, so a jsonPath() assertion on that response fails.

We can add more constraints and custom messages the same way.

Broken JSON also returns 400, for example {“title”: with no closing brace. In that case, Spring throws HttpMessageNotReadableException before validation runs.

2.8. Testing Exceptions Handled by @RestControllerAdvice

An advice class holds exception handlers that apply to all controllers. @WebMvcTest loads every @ControllerAdvice and @RestControllerAdvice class, so the test sees the same error responses as a real client. For example, a mobile app that asks for a deleted task gets the same 404 problem detail in production as the test does. Our advice maps TaskNotFoundException to 404.

@ExceptionHandler(TaskNotFoundException.class)
public ProblemDetail notFound(TaskNotFoundException ex) {
  return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}

The test makes the mock throw the exception and then checks the fields of the problem detail.

given(taskService.findById(99L)).willThrow(new TaskNotFoundException(99L));

mockMvc.perform(get("/tasks/{id}", 99))
    .andExpect(status().isNotFound())                                        // 404
    .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))    // application/problem+json
    .andExpect(jsonPath("$.title").value("Not Found"))
    .andExpect(jsonPath("$.detail").value("Task 99 not found"));
MockHttpServletResponse:
           Status = 404
    Error message = null
          Headers = [Content-Type:"application/problem+json"]
     Content type = application/problem+json
             Body = {"detail":"Task 99 not found","instance":"/tasks/99","status":404,"title":"Not Found"}

More REST API error handling patterns use the same advice class. FAQ 3.2 explains what MockMvc does with an exception that no handler catches.

2.9. The Same Tests With MockMvcTester

MockMvcTester wraps MockMvc in an AssertJ API (AssertJ is the library that provides assertThat(…) chains). @WebMvcTest and @AutoConfigureMockMvc create a MockMvcTester bean when AssertJ is on the classpath, and spring-boot-starter-test puts it there.

The test methods throw no checked exception, and after each dot, the IDE suggests the next assertion.

@WebMvcTest(TaskController.class)
class TaskControllerTesterTest {

  @Autowired MockMvcTester mvc;
  @MockitoBean TaskService taskService;

  @Test
  void getTaskById() {
    given(taskService.findById(1L)).willReturn(new Task(1L, "Buy milk", false));

    assertThat(mvc.get().uri("/tasks/{id}", 1))
        .hasStatusOk()
        .bodyJson()
        .extractingPath("$.title").isEqualTo("Buy milk");
  }

  @Test
  void createTaskWithBlankTitle() {
    assertThat(mvc.post().uri("/tasks")
            .contentType(MediaType.APPLICATION_JSON)
            .content("""
                {"title": "", "done": false}
                """))
        .hasStatus(HttpStatus.BAD_REQUEST)
        .bodyJson()
        .extractingPath("$.errors.title").isEqualTo("must not be blank");
  }
}

MockMvcTester can also turn the whole body into an object, and isEqualTo() compares that object with the expected one.

assertThat(mvc.get().uri("/tasks/{id}", 1))
    .hasStatusOk()
    .bodyJson()
    .convertTo(Task.class)
    .isEqualTo(new Task(1L, "Buy milk", false));

Each line in the classic style has a matching line in the AssertJ style.

MockMvcMockMvcTester
mockMvc.perform(get(“/tasks/{id}”, 1))assertThat(mvc.get().uri(“/tasks/{id}”, 1))
.andExpect(status().isOk()).hasStatusOk()
.andExpect(status().isBadRequest()).hasStatus(HttpStatus.BAD_REQUEST)
.andExpect(jsonPath(“$.title”).value(“Buy milk”)).bodyJson().extractingPath(“$.title”).isEqualTo(“Buy milk”)
.andExpect(header().string(“Location”, “/tasks/3”)).hasHeader(“Location”, “/tasks/3”)
.andDo(print()).apply(print())
throws Exception on every testNo checked exception

For new test classes, MockMvcTester is the better default, because when an assertion fails, AssertJ prints a message with the expected and the actual value. Existing MockMvc tests keep working, and one project can use both styles.

2.10. Running the Tests

The command mvn test runs four test classes. TaskControllerTest has 11 MockMvc tests, TaskControllerTesterTest has 5 MockMvcTester tests, and TaskControllerWithoutAdviceTest has 3 tests without the advice class (FAQ 3.2 and 3.3). The last class is the @SpringBootTest integration test.

[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.740 s -- in com.howtodoinjava.mockmvc.TaskControllerTesterTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.504 s -- in com.howtodoinjava.mockmvc.TaskControllerWithoutAdviceTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.428 s -- in com.howtodoinjava.mockmvc.TaskApiIntegrationTest
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.166 s -- in com.howtodoinjava.mockmvc.TaskControllerTest
[INFO] Tests run: 20, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

The first class is slow because the JVM and the Spring context start up, whereas TaskControllerTest runs 11 tests in 0.166 s. Spring caches the test context, so later classes with the same configuration and the same @MockitoBean fields reuse one context and skip the startup.

3. MockMvc FAQs

3.1. Why Does @WebMvcTest Not Find the Service Bean?

Because @WebMvcTest does not create @Service beans, Spring has no TaskService to pass into the controller’s constructor. When we copied the test class without the @MockitoBean field, the context failed to start.

java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@222a7429 testClass = com.howtodoinjava.mockmvc.TaskControllerNoMockTest, ...]
...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.howtodoinjava.mockmvc.TaskService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

The fix is one @MockitoBean field for each object the controller depends on. If the test needs the real service, @Import(TaskService.class) adds the bean to the slice, or we move the test to @SpringBootTest.

3.2. Why Does MockMvc Throw ServletException Instead of Returning 500?

Because MockMvc has no servlet container. In a running application, the servlet container (such as Tomcat) turns an unhandled exception into a 500 error page, but in a MockMvc test, the exception reaches the test method when no @ExceptionHandler matches. Our test without the advice class failed with a ServletException.

jakarta.servlet.ServletException: Request processing failed: com.howtodoinjava.mockmvc.TaskNotFoundException: Task 99 not found
...
Caused by: com.howtodoinjava.mockmvc.TaskNotFoundException: Task 99 not found

We have two options. We either add a handler for the exception (section 2.8), or we assert the exception itself, as TaskControllerWithoutAdviceTest in the example project does.

assertThatThrownBy(() -> mockMvc.perform(get("/tasks/{id}", 99)))
    .hasCauseInstanceOf(TaskNotFoundException.class);

3.3. Why Does a POST Return 400 When a Boolean Field Is Missing?

Because Jackson 3, the JSON library in Spring Boot 4, rejects the missing field. Our request {“title”: “”} had no done field, and the test got a 400 before validation ran.

By default, Jackson 3 fails when a JSON field for a primitive type (such as boolean or int) is missing or null. The log shows the reason.

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot map `null` into type `boolean` (set `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` to 'false' to allow)]

Jackson 2, used by Spring Boot 3, has the FAIL_ON_NULL_FOR_PRIMITIVES check off by default and sets the missing field to false. So we either send every primitive field in the test body, or declare the field as Boolean when the client may leave it out.

3.4. Should We Use MockMvc, MockMvcTester or RestTestClient?

All three can test a Spring MVC controller, and each one fits a different situation.

  • MockMvc fits existing test suites and Spring Boot 2 or 3 projects, because every version supports it.
  • MockMvcTester fits new tests on Spring Framework 6.2 or later (Spring Boot 3.4 or later), because its AssertJ assertions read better and need no throws Exception.
  • RestTestClient, new in Spring Framework 7, fits tests that must also run against a real server, because the same client can target MockMvc or a running application.

MockMvc does not work with a WebFlux application. For that stack, we use WebTestClient.

4. Conclusion

MockMvc sends a request through the real Spring MVC code inside the test’s JVM, so a controller test checks URL mapping, JSON conversion, validation and exception handlers without a server. @WebMvcTest creates only the web layer and needs a @MockitoBean for each service, whereas @SpringBootTest with @AutoConfigureMockMvc uses the real beans.

In Spring Boot 4, @MockitoBean replaces the removed @MockBean, and the test annotations moved to org.springframework.boot.webmvc.test.autoconfigure. MockMvcTester writes the same tests in AssertJ style. When a test fails, andDo(print()) shows the handler that ran and the resolved exception, along with the full response.

5. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. For one of my POST API I am geting such response, whats the possible cause??

    MockHttpServletResponse:
    Status = 200
    Error message = null
    Headers = []
    Content type = null
    Body =
    Forwarded URL = null
    Redirected URL = null
    Cookies = []

  2. org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.ClassCastException: org.springframework.web.servlet.resource.ResourceHttpRequestHandler cannot be cast to org.springframework.web.method.HandlerMethod

    I am getting this error, any idea what is wrong?

  3. Hey , can we mock methods using MockMVC , if no How can we mock a method with the similar approach ?? Pleas do a session on that as well . A typical example of a method call “addValues(int a, int b)” whichj the adds , subtracts , multiplies, and divides a with b , and combines the entire result as one string and returns it.

  4. Do you need to running service first to do Spring boot MockMvc tests ?

    I got assertion error on line “status().isOk()”

    • It is static method from MockMvcResultMatchers.
      Add the following to your REST API tests:

      import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
      import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

    • What error you are facing. I hope you have defined the method as given in post :

      public static String asJsonString(final Object obj) {
          try {
              return new ObjectMapper().writeValueAsString(obj);
          } catch (Exception e) {
              throw new RuntimeException(e);
          }
      }
      

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.