I'm trying to replace some of our MockMvc controller tests using MockMvcTester. With MockMvc, the @ControllerAdvice were picked up and used. For stand-alone you can .setControllerAdvice.

Using MockMvcTester, the @ControllerAdvice is not picked up, and I don't see a way to configure one. Is that on purpose? Or am I missing something?

Comment From: snicoll

Thanks for the report but I am not sure what you mean. MockMvcTester is based on MockMvc so whatever you could do with the latter you can do with the former. Can you show what you're trying to do and where exactly is the missing integration bit?

Comment From: HenrikPublic

Sure.

@SpringBootApplication
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/throw")
    public void throwException() {
        throw new TestException("test");
    }

    public static class TestException extends RuntimeException {
        public TestException(String message) {
            super(message);
        }
    }
}

@ControllerAdvice
public class TestControllerAdvice {
    @ResponseBody
    @ExceptionHandler({TestController.TestException.class})
    public ResponseEntity<TestError> toError(TestController.TestException e) {
        var error = new TestError(42, "Error from ControllerAdvice");
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }

    public record TestError(int errorCode, String message) {}
}

@WebMvcTest(TestController.class)
class TestControllerUnitTest {
    private final MockMvc mockMvc;
    private final MockMvcTester mockMvcTester;

    TestControllerUnitTest(@Autowired MockMvc mockMvc, @Autowired MockMvcTester mockMvcTester) {
        this.mockMvc = mockMvc;
        this.mockMvcTester = mockMvcTester;
    }

    @Test
    void with_mockmvc() throws Exception {
        mockMvc.perform(post("/test/throw"))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.errorCode", is(42)))
            .andExpect(jsonPath("$.message", is("Error from ControllerAdvice")));
    }

    @Test
    void with_mockmvctester() {
        var result = mockMvcTester.post().uri("/test/throw").exchange();
        assertThat(result)
            .hasFailed()
            .hasStatus(HttpStatus.BAD_REQUEST)
            .failure().hasMessage("Error from ControllerAdvice")
            .extracting("errorCode").isEqualTo(42);
    }
}

The with_mockmvc succeeds, while the with_mockmvctester fails with:

org.opentest4j.AssertionFailedError: 
Expecting message to be:
  "Error from ControllerAdvice"
but was:
  "test"

Comment From: snicoll

Can you move that to an actual application please? I'd rather spend time debugging the problem rather than piecing things together.

Comment From: HenrikPublic

Ahh, silly me 😊 I thought the idea was to extract the failure and assert on that one. But, in reality I produce a body using the @ControllerAdvice. So, the proper test could look something like:

    @Test
    void with_mockmvctester() {
        var result = mockMvcTester.post().uri("/test/throw").exchange();
        assertThat(result)
            .hasFailed()
            .hasStatus(HttpStatus.BAD_REQUEST)
            .bodyJson()
            .convertTo(TestError.class)
            .satisfies(error -> assertThat(error).isEqualTo(new TestError(42, "Error from ControllerAdvice")));
    }

Now it succeeds.

Amazing how trying to explain things makes you see your own errors...

Sorry about the noise.