27

My @ControllerAdvice annotated Controller looks like this:

@ControllerAdvice
public class GlobalControllerExceptionHandler {

    @ResponseStatus(value = HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(AuthenticationException.class)
    public void authenticationExceptionHandler() {
    }
}

Of course my development is test driven and I would like to use my exception Handler in the JUnit Tests. My Test case looks like this:

public class ClientQueriesControllerTest {

    private MockMvc mockMvc;

    @InjectMocks
    private ClientQueriesController controller;

    @Mock
    private AuthenticationService authenticationService;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
    }

    @Test
    public void findAllAccountRelatedClientsUnauthorized() throws Exception {
        when(authenticationService.validateAuthorization(anyString())).thenThrow(AuthenticationException.class);

        mockMvc.perform(get("/rest/clients").header("Authorization", UUID.randomUUID().toString()))
                .andExpect(status().isUnauthorized());
    }
}

Probably I need to register the ControllerAdvice Class. How to do that?

geoand
  • 60,071
  • 24
  • 172
  • 190
Rudolf Schmidt
  • 271
  • 1
  • 3
  • 3
  • Have you tried the `MockMvcBuilders.webAppContextSetup` option as described at http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/testing.html#spring-mvc-test-framework instead of `MockMvcBuilders.standaloneSetup`? – geoand Sep 01 '14 at 10:57

6 Answers6

38

Since Spring 4.2, you can register your ControllerAdvice directly into your StandaloneMockMvcBuilder:

MockMvcBuilders
     .standaloneSetup(myController)
     .setControllerAdvice(new MyontrollerAdvice())
     .build();
Morten Berg
  • 1,745
  • 16
  • 10
27

In order for the full Spring MVC configuration to get activated, you need to use MockMvcBuilders.webAppContextSetup instead of MockMvcBuilders.standaloneSetup.

Check out this part of the Spring documentation for more details.

Your code would look like:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-config.xml")
public class ClientQueriesControllerTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Autowired
    private AuthenticationService authenticationService;

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

    @Test
    public void findAllAccountRelatedClientsUnauthorized() throws Exception {
        when(authenticationService.validateAuthorization(anyString())).thenThrow(AuthenticationException.class);

        mockMvc.perform(get("/rest/clients").header("Authorization", UUID.randomUUID().toString()))
                .andExpect(status().isUnauthorized());
    }
}

Then inside test-config.xml you would add a Spring bean for AuthenticationService that is a mock.

<bean id="authenticationService" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="your.package.structure.AuthenticationService"/>
</bean>

You could of course use profiles to inject the mock AuthenticationService in the tests if want to reuse your regular Spring configuration file instead of creating test-config.xml.


UPDATE

After digging around a bit, I found that StandaloneMockMvcBuilder returned by (MockMvcBuilders.standaloneSetup) is totally customizable. That means that you can plug in whatever exception resolver you prefer.

However since you are using @ControllerAdvice, the code below will not work. If however your @ExceptionHandler method was inside the same controller the code all you would have to change is the following:

mockMvc = MockMvcBuilders.standaloneSetup(controller).setHandlerExceptionResolvers(new ExceptionHandlerExceptionResolver()).build();

UPDATE 2

Some more digging gave the answer to how you can register a correct exception handler when you are also using @ControllerAdvice.

You need to update the setup code in the test to the following:

    @Before
    public void setUp() throws Exception {
        final ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver = new ExceptionHandlerExceptionResolver();

        //here we need to setup a dummy application context that only registers the GlobalControllerExceptionHandler
        final StaticApplicationContext applicationContext = new StaticApplicationContext();
        applicationContext.registerBeanDefinition("advice", new RootBeanDefinition(GlobalControllerExceptionHandler.class, null, null));

        //set the application context of the resolver to the dummy application context we just created
        exceptionHandlerExceptionResolver.setApplicationContext(applicationContext);

        //needed in order to force the exception resolver to update it's internal caches
        exceptionHandlerExceptionResolver.afterPropertiesSet();

        mockMvc = MockMvcBuilders.standaloneSetup(controller).setHandlerExceptionResolvers(exceptionHandlerExceptionResolver).build();
    }
geoand
  • 60,071
  • 24
  • 172
  • 190
  • Thanks for your post, it helped alot. But is there another more simpler solution than webApplicationContext? I tried to annotate the class AuthenticationException directly and it works with standaloneserver. The problem is that I want to seperate the rest from the service layer and not mix both layers with annotations from the another layer. The exception handling should be managed in the rest layer. Is there another possiblity? – Rudolf Schmidt Sep 01 '14 at 11:48
  • @RudolfSchmidt I'm glad it helped you! You are correct to want to separate concerns! But I don't understand what the problem is with `webApplicationContext`. Can you elaborate a little on that? I am not aware of any other solution that would register the exception handling correctly in a test environment – geoand Sep 01 '14 at 11:57
  • @RudolfSchmidt I found some more info that might help. Will update shortly – geoand Sep 01 '14 at 12:03
  • 2
    Unfortunately its not working, getting org.springframework.web.util.NestedServletException: Request processing failed; nested exception is AuthenticationException, there is no problem to use webApplicationContext, buts its a little bit slower, and more code to use (more annotations). the other solution is to annotate the exception class directly, but its not clean. Hope there is another workaround. – Rudolf Schmidt Sep 01 '14 at 12:34
  • @RudolfSchmidt I found that with the solution above, the `@ExceptionHandler` works if it's in the same controller, not in a `@ControllerAdvice`. I will do some more digging to see what I can come up with – geoand Sep 01 '14 at 12:36
  • @RudolfSchmidt This is a actually an awesome exercise in the internals of Spring! – geoand Sep 01 '14 at 12:45
  • @RudolfSchmidt Just a little more patience :) I'm very close – geoand Sep 01 '14 at 13:14
  • @RudolfSchmidt Check out my latest update. It should work for you now! – geoand Sep 01 '14 at 13:22
  • @JeeBee Great to hear! – geoand Jun 17 '15 at 07:30
20

Got past the NestedServletException with the following solution...

    final StaticApplicationContext applicationContext = new StaticApplicationContext();
    applicationContext.registerSingleton("exceptionHandler", GlobalControllerExceptionHandler.class);

    final WebMvcConfigurationSupport webMvcConfigurationSupport = new WebMvcConfigurationSupport();
    webMvcConfigurationSupport.setApplicationContext(applicationContext);

    mockMvc = MockMvcBuilders.standaloneSetup(controller).
        setHandlerExceptionResolvers(webMvcConfigurationSupport.handlerExceptionResolver()).
        build();
JJZCorum
  • 203
  • 1
  • 5
  • 1
    Thanks for your answer! Based on it I've found myself a solution for the RestAssuredMockMvc approach: http://stackoverflow.com/a/38435008/2239713 – ilyailya Jul 26 '16 at 12:52
  • this doesn't work anymore. Since Spring v5.x webMvcConfigurationSupport.handlerExceptionResolver() requires an argument. – saran3h Aug 07 '21 at 06:14
0

If you have multiple advice classes, each with @ExceptionHandler and one of those classes is handling a very generic base exception, like @ExceptionHandler({Exception.class}), then you will need to add some priority ordering to your advice classes per this SO answer

https://stackoverflow.com/a/19500823/378151

Snekse
  • 15,474
  • 10
  • 62
  • 77
0

If you are using junits older version than 5 and can not upgrade it for any reason then consider defining like below:

@Before
    public void setUp() throws Exception
    {
        MockitoAnnotations.initMocks(this);       
        final StaticApplicationContext applicationContext = new StaticApplicationContext();
        applicationContext.registerSingleton("exceptionHandler", MyExceptionHandler.class);

        final WebMvcConfigurationSupport webMvcConfigurationSupport = new WebMvcConfigurationSupport();
        webMvcConfigurationSupport.setApplicationContext(applicationContext);

        mockMvc = MockMvcBuilders.standaloneSetup(myController).
            setHandlerExceptionResolvers(webMvcConfigurationSupport.handlerExceptionResolver()).
            build();
Kusum
  • 241
  • 5
  • 20
-1

You can add this to your test class

@Autowired
@Qualifier("handlerExceptionResolver")
void setExceptionResolver(HandlerExceptionResolver resolver)
{
    this.exceptionResolver = resolver;
}

and then add the exceptionResolver to your MockMvc

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
    mockMvc = MockMvcBuilders.standaloneSetup(controller)
               .setHandlerExceptionResolvers(this.exceptionResolver).build();
}
  • 1
    To me it does not seem to make sense to use @Autowired in standalone mode. You should not even have a spring context in standalone mode – Bastian Voigt Jan 26 '16 at 09:53