Why ExceptionHandler does not work with junit?

62 views Asked by At

I am having trouble to test a controller class with @ExceptionHandler annotation.

At first, my controller was like this :

@RestController
@RequestMapping("/api/user")
@Tag(name = "User")
public class UserRestController {
    private final UserService userService;
    
    @Autowired
    public UserRestController(UserService userService){
        this.userService = userService;
    }

    @PutMapping("/update")
    @Operation(summary = "Update a User",
        requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
            description = "User to update",
            required = true,
            content = @Content(schema = @Schema(implementation = User.class))
    ))
    @ApiResponses(
        value = {
            @ApiResponse(responseCode = "200", description = "Update Ok",
                content = { @Content(schema = @Schema(implementation = User.class))}),
            @ApiResponse(responseCode = "404", description = "Ressource not found",
                content = @Content),
            @ApiResponse(responseCode = "400", description = "Invalid input",
                content = @Content),
            @ApiResponse(responseCode = "500", description = "Internal server error",
                content = @Content),
        }
    )
    public ResponseEntity<User> updateUser (@RequestBody User user) {

        try {
            userService.getUserById(user.getIdUser());
            User newUser = userService.updateUser(user);
            return ResponseEntity.status(HttpStatus.OK).body(newUser);
        } catch (NotFoundException e) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        } catch (IllegalArgumentException e) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

And my test :

@SpringBootTest(classes = MyApp.class)
@ExtendWith(MockitoExtension.class)
class UserRestControllerTest {

    @Autowired
    private UserRestController userController;

    @MockBean
    private UserService userService;

    private User user1;
    private User user2;

    @BeforeEach
    public void setUp() {
        user1 = new User();
        user1.setIdUser(1L);

        user2 = new User();
        user2.setIdUser(2L);
    }

    @Test
    void testUpdateUserInternalServerError() {
        Mockito.when(userService.updateUser(user1)).thenThrow(new RuntimeException());

        ResponseEntity<User> response = userController.updateUser(user1);
        Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
    }
}

Here everything worked just fine, but I had a lot of duplication in my controller due to several methods with try/catch blocks, so I modified it to have :

@RestController
@RequestMapping("/api/user")
@Tag(name = "User")
public class UserRestController {
    private final UserService userService;
    
    @Autowired
    public UserRestController(UserService userService){
        this.userService = userService;
    }

    @PutMapping("/update")
    @Operation(summary = "Update a User",
        requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
            description = "User to update",
            required = true,
            content = @Content(schema = @Schema(implementation = User.class))
    ))
    @ApiResponses(
        value = {
            @ApiResponse(responseCode = "200", description = "Update Ok",
                content = { @Content(schema = @Schema(implementation = User.class))}),
            @ApiResponse(responseCode = "404", description = "Ressource not found",
                content = @Content),
            @ApiResponse(responseCode = "400", description = "Invalid input",
                content = @Content),
            @ApiResponse(responseCode = "500", description = "Internal server error",
                content = @Content),
        }
    )
    public ResponseEntity<User> updateUser (@RequestBody User user) {
            userService.getUserById(user.getIdUser());
            User newUser = userService.updateUser(user);
            return ResponseEntity.status(HttpStatus.OK).body(newUser);
    }

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity handleException(Exception e) {
        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

When I start my application, everything is good, the exceptionHandler do its job and send a 500 response to my front app. The problem is when I launch my test class I have an error :

10:55:49.308 [main] DEBUG o.s.t.c.s.DirtiesContextTestExecutionListener - After test method: class [UserRestControllerTest], method [testUpdateUserInternalServerError], class annotated with @DirtiesContext [false] with mode [null], method annotated with @DirtiesContext [false] with mode [null]
10:55:49.308 [main] DEBUG o.s.t.c.w.ServletTestExecutionListener - Resetting RequestContextHolder for test class com.open.myApp.back.controllers.UserRestControllerTest

java.lang.RuntimeException
    at com.open.myApp.back.services.UserServiceImpl.updateUser(UserServiceImpl.java:49)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:704)
    at com.open.myApp.back.services.UserServiceImpl$$SpringCGLIB$$0.updateUser(<generated>)
    at com.open.myApp.back.controllers.UserRestController.updateUser(UserRestController.java:81)
    at com.open.myApp.back.controllers.UserRestControllerTest.testUpdateUserInternalServerError(UserRestControllerTest.java:112)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
    at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
    at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
    at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

Does anyone have an idea on how to deal with that ?

Thanks for your Help.

I tried to mock the response of my repo instead of the one of my service, but the problem was still there.

Problem solved!

Thanks to the explanations of M. Deinum (see comments), I figured out what was the deal. Here is my class test using MockMvc :

@SpringBootTest(classes = MyApp.class)
@AutoConfigureJsonTesters
@AutoConfigureMockMvc
class UserRestControllerTest {

    @MockBean
    private UserService userService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private JacksonTester<User> jsonUser;

    private MockMvc mockMvc;

    private static final String USER_1 = """
            {
                "idUser": "1"
            }
            """;

    private User user1;
    private User user2;

    @BeforeEach
    public void setUp() {
        user1 = new User();
        user1.setIdUser(1L);

        user2 = new User();
        user2.setIdUser(2L);

        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
    
    @Test
    void testUpdateUserInternalServerError() throws Exception {
        Mockito.when(userService.updateUser(user1)).thenThrow(new RuntimeException());

        MockHttpServletResponse response = this.mockMvc.perform(put("/api/user/update")
                        .contentType(APPLICATION_JSON)
                        .content(USER_1)
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), response.getStatus());
    }
}
2

There are 2 answers

0
alexid On

As per Spring docs:

If an exception occurs during request mapping or is thrown from a request handler (such as a @Controller), the DispatcherServlet delegates to a chain of HandlerExceptionResolver beans to resolve the exception and provide alternative handling, which is typically an error response.

In other words, you need a bit more "Spring magic" than just your own controller to be able to process an exception in a proper handler. That's why you should use something like MockMvc

0
Enialix On

Problem solved!

Thanks to the explanations of M. Deinum (see comments), I figured out what was the deal. Here is my class test using MockMvc :

@SpringBootTest(classes = MyApp.class)
@AutoConfigureJsonTesters
@AutoConfigureMockMvc
class UserRestControllerTest {

    @MockBean
    private UserService userService;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private JacksonTester<User> jsonUser;

    private MockMvc mockMvc;

    private static final String USER_1 = """
            {
                "idUser": "1"
            }
            """;

    private User user1;
    private User user2;

    @BeforeEach
    public void setUp() {
        user1 = new User();
        user1.setIdUser(1L);

        user2 = new User();
        user2.setIdUser(2L);

        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }
    
    @Test
    void testUpdateUserInternalServerError() throws Exception {
        Mockito.when(userService.updateUser(user1)).thenThrow(new RuntimeException());

        MockHttpServletResponse response = this.mockMvc.perform(put("/api/user/update")
                        .contentType(APPLICATION_JSON)
                        .content(USER_1)
                        .accept(MediaType.APPLICATION_JSON))
                .andReturn().getResponse();

        Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), response.getStatus());
    }
}