Volkan Yazici opened SPR-12692 and commented

MockMvc fails to capture the message field of exceptions thrown by a controller; whereas, the same message gets captured perfectly well by the application server. That is, given the following controller:

@RestController
public class Controller {

    @RequestMapping("/test")
    public void create() {
        throw new BadRequestException("it works");
    }

}

such that BadRequestException is defined as:

@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "bad request")
public class BadRequestException extends IllegalArgumentException {

    public BadRequestException(String message) { super(message); }

}

the unit test shown below

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { Main.class })
@WebAppConfiguration
public class ControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void testUserNewWithInvalidInput() throws Exception {
        mockMvc.perform(get("/test")).andDo(print());
    }

}

shows that MockMvc did not capture the response body:

...
MockHttpServletResponse:
              Status = 400
       Error message = bad request
             Headers = {}
        Content type = null
                Body = 
       Forwarded URL = null
      Redirected URL = null
             Cookies = []

However, the same controller works perfectly fine when run within a Servlet container:

$ curl -v http://localhost:8080/test
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8080
> Accept: */*
> 
< HTTP/1.1 400 Bad Request
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Thu, 05 Feb 2015 13:40:46 GMT
< Connection: close
< 
* Closing connection 0
{"timestamp":1423143646099,"status":400,"error":"Bad Request","exception":"com.vlkan.springtest.BadRequestException","message":"bad request","path":"/test"}

Affects: 4.1.4

0 votes, 6 watchers

Comment From: spring-projects-issues

Rossen Stoyanchev commented

I think the error message in the body is taken care of by Spring Boot which configures error mappings at the Servlet container level (see here) and since Spring MVC Test runs with a mock Servlet request/response, there is no such error mapping. I think the exception is handled instead by the ResponseStatusExceptionResolver (although I haven't verified this).

Given that this is Spring Boot provided error handling -- in other words it's not application logic, my recommendation is create at least one integration test (see Boot's documentation on @WebIntegrationTest) and stick to Spring MVC Test for your controller logic.

Comment From: spring-projects-issues

Volkan Yazici commented

Using an integration test indeed solves the problem, but I would feel happier if I could have done that using regular MockMvc unit tests too. Anyway, thanks for the tip.

Comment From: spring-projects-issues

Rossen Stoyanchev commented

Given there is no Servlet container involved, I'm afraid something like this is simply not in scope.

Comment From: spring-projects-issues

Alper Kanat commented

Hi @Rossen,

I'm affected by this problem as well. I wouldn't care for error response body since it's returning a proper HTTP status code which I can test against but I started to use Spring REST Docs and now I can't document the API because of this.

Comment From: spring-projects-issues

member sound commented

You might just use: .andReturn().getResolvedException().getMessage();

Comment From: rubensa

member sound commented

You might just use: .andReturn().getResolvedException().getMessage();

This only works if your Exception is annotated with @ResponseStatus as, then, the ResponseStatusExceptionResolver handles the exception. If the Exception is not annotated, then, it is thrown encapsulated inside a NestedServletException.

In that case you can test it with something like:

package org.eu.rubensa.springboot.error;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Using this annotation will disable full auto-configuration and instead apply
 * only configuration relevant to MVC tests
 * (i.e. @Controller, @ControllerAdvice, @JsonComponent,
 * Converter/GenericConverter, Filter, WebMvcConfigurer and
 * HandlerMethodArgumentResolver beans but not @Component, @Service
 * or @Repository beans).
 * <p>
 * By default, tests annotated with @WebMvcTest will also auto-configure
 * MockMvc.
 * <p>
 * For more fine-grained control of MockMVC the @AutoConfigureMockMvc annotation
 * can be used.
 * <p>
 * By default MockMVC printOnlyOnFailure = true so information is printed only
 * if the test fails.
 */
@WebMvcTest()
public class MockMvcNestedServletExceptionTest {
  /**
   * MockMvc is not a real servlet environment, therefore it does not redirect
   * error responses to ErrorController, which produces error response.
   * <p>
   * See: https://github.com/spring-projects/spring-boot/issues/5574
   */
  @Autowired
  private MockMvc mockMvc;

  @Test
  public void testRuntimeException() throws Exception {
    Assertions
        .assertThatThrownBy(
            () -> mockMvc.perform(MockMvcRequestBuilders.get("/exception").contentType(MediaType.APPLICATION_JSON)))
        .hasCauseInstanceOf(RuntimeException.class).hasMessageContaining("The exception message");
  }

  /**
   * A nested @Configuration class wild be used instead of the application’s
   * primary configuration.
   * <p>
   * Unlike a nested @Configuration class, which would be used instead of your
   * application’s primary configuration, a nested @TestConfiguration class is
   * used in addition to your application’s primary configuration.
   */
  @Configuration
  /**
   * Tells Spring Boot to start adding beans based on classpath settings, other
   * beans, and various property settings.
   */
  @EnableAutoConfiguration
  /**
   * The @ComponentScan tells Spring to look for other components, configurations,
   * and services in the the TestWebConfig package, letting it find the
   * TestController class.
   * <p>
   * We only want to test the classes defined inside this test configuration so
   * not using it.
   */
  static class TestConfig {
    @RestController
    public class TestController {
      @GetMapping("/exception")
      public void getException() {
        throw new RuntimeException("The exception message");
      }
    }
  }
}