SpringBootTest with MockMvcBuilders stand alone setup is not loading my ControllerAdvice despite setting it

25k views Asked by At

I am creating my controller and controller advice like this:

Test class:

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestController {

    private MockMvc mockMvc;

    @Mock
    private MyService myService;

    @Autowired
    @InjectMocks
    private MyController myController;

    @Before
    public void setup() {

        MockitoAnnotations.initMocks(this);

        //Build the controller mock handler
        mockMvc = MockMvcBuilders
            .standaloneSetup(MyController.class)
            .setControllerAdvice(new MyControllerAdvice())

            //This also doesn't work
            //.setHandlerExceptionResolvers(createExceptionResolver())
            .build();
    }

    //This also did not work
    private ExceptionHandlerExceptionResolver createExceptionResolver() {
        ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {
            protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
                Method method = new ExceptionHandlerMethodResolver(MyControllerAdvice.class).resolveMethod(exception);
                return new ServletInvocableHandlerMethod(new MyControllerAdvice(), method);
            }
        };
        exceptionResolver.afterPropertiesSet();
        return exceptionResolver;
    }

    /**
     * Tests passing bad input to see if our exception handler is called.
     */
    @Test
    public void testBadRequest()
    {
        //Make a request object that has a bad input (e.g. bad date string)
        MyRequest request = new MyRequest();

        //Set the request values
        request.setDate( "a" );

        try
        {
            myController.getSomething( request );
        }
        catch (Exception e)
        {
            //It reaches here without ever reaching my controller advice in debugging
            e.printStackTrace();
        }
    }
}

Controller advice:

@EnableWebMvc
@ControllerAdvice
@Component
public class MyControllerAdvice {

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity<String> handleException(HttpServletRequest request, Exception exception) throws Exception
    {
        //This is never called (I'm using a debugger and have a breakpoint here)
        return new ResponseEntity<String>(
            "test",
            HttpStatus.INTERNAL_SERVER_ERROR
        );
    }
}
2

There are 2 answers

0
Anatoly Shamov On BEST ANSWER

There are two issues in your example:

  1. MockMvcBuilders#standaloneSetup() receives Controller objects as parameters, not the Class objects. So it should be:

    mockMvc = MockMvcBuilders
            .standaloneSetup(new MyController())
            .setControllerAdvice(new MyControllerAdvice())
            .build();
    
  2. You are calling myController.getSomething( request ) directly, while you should use previously built mockMvc. Direct call is unadvised as it's not processed with TestDispatcherServlet. Here is a couple of examples for mockMvc requests:

GET

    mockMvc.perform(get("/testSomething"))
            .andExpect(status().is5xxServerError())
            .andReturn();

POST

    mockMvc.perform(post("/testSomething")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(json)) //it's JSON string
            .andExpect(status().is5xxServerError())
            .andReturn();
0
Emanuel Trandafir On

You are trying to serialize to json a mocked bean. To simplify things, run this test, and you'll get the same error:

@Test
public void test() throws Exception {
    var dto = Mockito.mock(EmployeeDTO.class);
    var mapper = new ObjectMapper();
    mapper.writeValueAsString(dto); // will throw the same exception
}

To fix it, you might need to rethink your design. Why are you declaring this DTO as a @Component and why do you mock it? Perhaps a better solution would be to have to keep this DTO as a POJO (Plain Old Java Object) and use a Service to will retrieve it. This way you will be able to mock the service and force it to return a specific employee for tests.

As a rule of thumb, we should mock roles/behavior, not data.