spring-boot: MockMvc doesn't use spring-boot's mvc exception handler

I’m trying to test my Controller using MockMvc. The service used by Controller throws RuntimeException if something is wrong. The spring-boot’s default exception resolver catches this exception and responds with status 500. But when I use MockMvc in my test, the test method just throws the exception instead of catch status 500. If I add explicit exception resolver using @ExceptionHandler, then test method starts to work.

The simple example. My Controller just throws new RuntimeException() for simplicity.

@RunWith(SpringRunner.class)
@WebMvcTest(MyController.class)
public class ControllerTests {

	@Autowired
	private MockMvc mvc;

	@Test
	public void testInternalServerError() throws Exception {
		mvc.perform(get("/api/data"))
				.andExpect(status().isInternalServerError());
	}
}

Expected result: Test catches status 500 and passes.

Actual result: The exception is thrown and test fails.

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException

Maybe I’m wrong assuming that spring-boot installs some default mvc exception handler. But anyway the real application returns status 500 with properly formatted JSON of error details. So I think the MockMvc should do the same.

I can add the reproducible example if someone confirms that this is really not intended behaviour.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 27
  • Comments: 17 (8 by maintainers)

Most upvoted comments

Spring Boot’s error handling is based on Servlet container error mappings that result in an ERROR dispatch to an ErrorController. MockMvc however is container-less testing so with no Servlet container the exception simply bubbles up with nothing to stop it.

So MockMvc tests simply aren’t enough to test error responses generated through Spring Boot. I would argue that you shouldn’t be testing Spring Boot’s error handling. If you’re customizing it in any way you can write Spring Boot integration tests (with an actual container) to verify error responses. And then for MockMvc tests focus on fully testing the web layer while expecting exceptions to bubble up.

This is a typical unit vs integration tests trade off. You do unit tests even if they don’t test everything because they give you more control and run faster.

Thanks, @rstoyanchev.

How can we write tests for error conditions using default spring-boot JSON responses, then?

@xak2000 Rossen’s already covered this, but I wanted to give you a direct answer. If you really want to test the precise format of the error response then you can use an integration test using @SpringBootTest configured with a DEFINED_PORT or RANDOM_PORT web environment and TestRestTemplate.

maybe this helps with a MockMvc approach:

//... fooMockMvc
        .andReturn()
        .getResolvedException()
        .getMessage();

source: https://github.com/spring-projects/spring-framework/issues/17290#issuecomment-453422142

maybe this helps with a MockMvc approach:

//... fooMockMvc
        .andReturn()
        .getResolvedException()
        .getMessage();

source: spring-projects/spring-framework#17290 (comment)

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");
      }
    }
  }
}

the problem with using TestRestTemplate is that you loose the ability to setup your test data in a @Transactional integration test inside the test method, therefore, keeping the database clean for other tests to run. TestRestTemplate causes the request to be processed in a different thread and therefore, you have to persistently modify the database which is a pain to clean up. I would be ok if the response is not the EXACT same for MockMvcTests, but at least the HTTP status returned should be honoured.

Unfortunately TestRestTemplate doesn’t work well with Spring REST Docs. 😞

And just to give visibility in terms of implementation, that’s how I’m doing:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment  = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SizesRestControllerIT {

    @LocalServerPort
    int port;

    @Before
    public void setUp() {
        RestAssured.port = port;
    }

    @Test
    public void test2() throws InterruptedException {
        given().basePath("/clothes").get("").then().statusCode(200);
    }
}