Spring MVC Controllers Unit Test not calling @ControllerAdvice
Asked Answered
P

14

96

I have a set of Controllers in the application and a class annotated as @ControllerAdvice which sets up certain data elements that are used in each of these controllers. I'm using Spring MVC 3.2 and have Junits for these controllers. When I run the Junit the control is not going to the ControllerAdvice class wheres it works fine if I deploy the app in Tomcat and submit a request through browser.

Any thoughts please?.

Piacular answered 8/3, 2013 at 19:57 Comment(4)
Appreciate an update on this. I am facing similar issue where @ExceptionHandler(Exception.class) annotated method in a @ControllerAdvice class is not called via unit test - webAppContextSetup(wac).build(). ExceptionHandler annotated method is called when deployed as webapp.Pothead
The correct response to this problem has changed over time. See @Matt Byrne's answer towards the end: https://mcmap.net/q/217777/-spring-mvc-controllers-unit-test-not-calling-controlleradviceTorse
The same issue is happening for WebTestClient call. Any solution?Boy
Make sure the exception is not thrown in the filter chain.Equate
H
146

After using the answer from @eugene-to and another similar one here I found limitations and raised an issue on Spring: https://jira.spring.io/browse/SPR-12751

As a result, Spring test introduced the ability to register @ControllerAdvice classes in the builder in 4.2. If you are using Spring Boot then you will need 1.3.0 or later.

With this improvement, if you are using standalone setup then you can set one or more ControllerAdvice instances like so:

mockMvc = MockMvcBuilders.standaloneSetup(yourController)
            .setControllerAdvice(new YourControllerAdvice())
            .build();

Note: the name setControllerAdvice() may not make it immediately obvious but you can pass many instances to it, since it has a var-args signature.

Hugh answered 25/2, 2015 at 19:24 Comment(5)
I Add something for thoses, Like me, who are annoyed by the spring validator with the trick above : this.mockMvc = MockMvcBuilders.standaloneSetup(yourController) .setValidator(mock(Validator.class)) .setControllerAdvice(new CaissierExceptionHandler()) .build();Sardius
If you're using webAppContextSetup instead of standaloneSetup, you may have forgotten @EnableWebMvc annotation. See answer below by tunguski https://mcmap.net/q/217777/-spring-mvc-controllers-unit-test-not-calling-controlleradvice.Writing
setContollerAdvice is not working when i was using standaloneSetup in Spring 4.2.X version.Lacrimator
What if the advice applies for all controllers ? Do i have to register them manually one by one ?Henequen
The same issue is happening for WebTestClient call. Any solution?Boy
E
42

Suppose you have class MyControllerAdvice annotated with @ControllerAdvice that has methods annotated with @ExceptionHandler. For MockMvc you can easily add this class as exception resolver.

@Before
public void beforeTest() {
    MockMvc mockMvc = standaloneSetup(myControllers)
        .setHandlerExceptionResolvers(createExceptionResolver())
        .build();
}

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;
}
Exhibitioner answered 12/10, 2013 at 19:39 Comment(2)
this does not work for me using async request handlingAffine
Great answer, but you will also need to set the MessageConverters for your exceptionResolver if your resolver returns any kind of object. See this answer for an example: https://mcmap.net/q/219229/-testing-restcontroller-with-controlleradviceHugh
G
27

I had similar problem when trying to test ExceptionHandler annotated with @ControllerAdvice. In my case I had to add @Configuration file with @EnableWebMvc annotation to @ContextConfiguration on test class.

So my test looked like this:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {
  RestProcessingExceptionHandler.class,
  TestConfiguration.class,
  RestProcessingExceptionThrowingController.class })
public class TestRestProcessingExceptionHandler {

  private MockMvc mockMvc;
  @Autowired
  WebApplicationContext wac;

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

  @Configuration
  // !!! this is very important - conf with this annotation 
  //     must be included in @ContextConfiguration
  @EnableWebMvc
  public static class TestConfiguration { }

  @Controller
  @RequestMapping("/tests")
  public static class RestProcessingExceptionThrowingController {
    @RequestMapping(value = "/exception", method = GET)
    public @ResponseBody String find() {
      throw new RestProcessingException("global_error_test");
    }
  }

  @Test
  public void testHandleException() throws Exception {
    mockMvc.perform(get("/tests/exception"))
      .andExpect(new ResultMatcher() {
        @Override
        public void match(MvcResult result) throws Exception {
          result.getResponse().getContentAsString().contains("global_error_test");
        }
      })
      .andExpect(status().isBadRequest());
  }
}

With @EnableWebMvc configuration my test passed.

Grettagreuze answered 20/9, 2013 at 7:38 Comment(2)
java.lang.IllegalStateException: Failed to load ApplicationContext :(Sick
This worked for me, but I was able to just add @EnableWebMvc to the test class itself rather than including the TestConfiguration class, i.e.: @WebAppConfiguration @ContextConfiguration ... @EnableWebMvc public class TestRestProcessingExceptionHandler {Goltz
F
21

This code is working for me:

public class MyGlobalExceptionHandlerTest {

    private MockMvc mockMvc;

    @Mock
    HealthController healthController;

    @BeforeTest
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(healthController)
            .setControllerAdvice(new GlobalExceptionHandler())
            .build();
    }

    @Test(groups = { "services" })
    public void testGlobalExceptionHandlerError() throws Exception {
        Mockito.when(healthController.health())]
               .thenThrow(new RuntimeException("Unexpected Exception"));
        mockMvc.perform(get("/health")).andExpect(status().is(500));
    }
}
Filing answered 11/7, 2017 at 13:28 Comment(2)
what version of spring boot did that work with? with 1.5.6 this code never calls my controlleradvicePitarys
@Bikesh , it worked for me , thanks a lot , you saved my time .Likker
W
7

I've been struggling with the same thing for quite some time. After much digging, the best reference was the Spring documentation:

http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/testing.html#spring-mvc-test-framework

In short, if you are simply testing a controller and its methods then you can use the 'standaloneSetup' method which creates a simple Spring MVC configuration. This will not include your error handler that you annotate with @ControllerAdvice.

private MockMvc mockMvc;

@Before
public void setup() {
    this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}

// ...

To create a more complete Spring MVC configuration that does contain your error handler you should use the following setup:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Autowired
    private AccountService accountService;

    // ...

}
What answered 8/4, 2013 at 10:27 Comment(3)
You still need to initialise the mockMvc, which is not shown in the above example: @Before public void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); }Plexus
You can use .setControllerAdvice(xxx) with the StandaloneMockMvcBuilder to inject your ControllerAdvice beanLanettelaney
@Lanettelaney saved my life. This is the simplest solution. mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new MyAppExceptionHandler()).build();Duero
H
4

The ControllerAdvice should be picked up by @WebMvcTest, see also Spring-Doc Works so far for me.

Example:

@RunWith(SpringRunner.class)
@WebMvcTest(ProductViewController.class)
Huba answered 10/3, 2020 at 9:6 Comment(3)
This works for me with spring-boot 2.3 and Junit 5. Even @RunWith(SpringRunner.class) is not needed,Outleap
It does work with JUnit-5, but in my case not inside the parameterized tests...cant figure out why... @smile any ideas why? have you tried that?Purvey
@Purvey would need to look at code. May be you can ask a new question with code and complete details?Outleap
B
2

When using @WebMvcTest with specific controllers, controller advice will not be used by the spring configuration: https://github.com/spring-projects/spring-boot/issues/12979.

You can explicitly tell the spring, via the @Import annotation, to use the controller advice:

@WebMvcTest(controllers = AppController.class)
@Import(AppControllerAdvice.class)
class AppControllerTest {

    @Autowired
    private MockMvc mockMvc;
}
Brumley answered 25/10, 2022 at 8:48 Comment(1)
Instead of the most voted solution (Setting up the MockMvc using the builder) this @Import was the solution for me. When i was using the MockMvcBuilders my custom WebMvcConfigurer implementation was not picked up.Ganiats
K
1

@tunguski sample code works but it pays to understand how things work. This is just one way to set things up.

@EnableWebMvc is equivalent to following in a spring configuration file

<mvc:annotation-driven />

Essentially for things to work you need to initialize Spring Mvc and load all your controllers and bean references. So following could be a valid setup as well as an alternate

Following is how you would setup the test class

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = { "classpath: "classpath:test-context.xml" })
    @WebAppConfiguration    
    public class BaseTest {

        @Autowired
        WebApplicationContext wac;

        private MockMvc mockMvc;

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

And following could be the spring configuration for the test

<mvc:annotation-driven />
<context:component-scan base-package="com.base.package.controllers" />
Kinnie answered 20/8, 2014 at 18:46 Comment(0)
S
1

I encountered this issue while writing controller tests with spock (groovy). My test class was originally written like:

@AutoConfigureMockMvc(secure = false)
@SpringBootTest
@Category(RestTest)
class FooControllerTest extends Specification {
  def fooService = Mock(FooService)
  def underTest = new FooController(FooService)
  def mockMvc = MockMvcBuilders.standaloneSetup(underTest).build()
....
}

This caused ControllerAdvice to be ignored. Changing the code to to Autowire the mocks fixed the problem.

@AutoConfigureMockMvc(secure = false)
@SpringBootTest
@Category(RestTest)
class FooControllerTest extends Specification {

  @AutowiredMock
  FooService FooService

  @Autowired
  MockMvc mockMvc
Snippet answered 30/10, 2017 at 4:50 Comment(0)
A
0

You would need to provide more info, and maybe some actual code and/or config files, before you can expect specific answers. That said, based on the little you have provided, it sounds like the annotated bean is not being loaded.

Try adding the following to your test applicationContext.xml (or equivalent spring config file, if you are using one).

<context:component-scan base-package="com.example.path.to.package" />

Alternatively, you may need to 'manually' load the contexts within the tests by including the following annotations before your test class:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/applicationContext.xml")

Good luck!

Allamerican answered 8/3, 2013 at 20:22 Comment(0)
K
0

I suspect you need to use asyncDispatch in your test; the regular testing framework is broken with asynchronous controllers.

Try the approach in: https://github.com/spring-projects/spring-framework/blob/master/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java

Katiakatie answered 17/5, 2019 at 8:45 Comment(0)
L
0

The simplest way it's to add Your @ControllerAdvice annotated class to @ContextConfiguration.

I had to change from this

@AutoConfigureMockMvc
@ContextConfiguration(classes = OrderController.class)
@WebMvcTest
class OrdersIntegrationTest

to this:

@AutoConfigureMockMvc
@ContextConfiguration(classes = {OrderController.class, OrdersExceptionHandler.class})
@WebMvcTest
class OrdersIntegrationTest
Laughable answered 29/12, 2020 at 15:20 Comment(0)
N
0

I am using Spring Boot 2.x, but it seems MockMvcBuilders is not required anymore or as we are defining the ControllerAdvice as part of the Configuration, it gets loaded.

@WebMvcTest
@ContextConfiguration(classes = {
  UserEndpoint.class, //the controller class for test
  WebConfiguration.class, //security configurations, if any
  StandardRestExceptionInterpreter.class. //<-- this is the ControllerAdvice class
})
@WithMockUser(username = "[email protected]", authorities = {"DEFAULT"})
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserEndpointTests {

@Test
@Order(3)
public void shouldThrowExceptionWhenRegisteringDuplicateUser() throws Exception {
    //do setup...
    Mockito.doThrow(EntityExistsException.class).when(this.userService).register(user);

    this.mockMvc
            .perform(MockMvcRequestBuilders
                    .post("/users")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(this.objectMapper.writeValueAsString(user)))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(MockMvcResultMatchers.status().isConflict());
    }
}
Nahamas answered 29/5, 2021 at 5:44 Comment(0)
I
-1

Just had the same issue, but solved it by adding the advicer to the classes in the @SpringBootTest:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MyController.class, MyControllerAdvice.class})
@AutoConfigureMockMvc(secure = false)
@ContextConfiguration(classes = {MyTestConfig.class})
@EnableWebMvc
Itinerary answered 17/9, 2021 at 12:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.