1 2 3 4 5 6 7 8 9 10 11 |
@RequestMapping(value = "/test", method = RequestMethod.PUT) @ResponseBody public Info find(@PathVariable String id) throws Exception { Info info = infoRepository.select(id); if (info == null) { throw new CustomException("NOT_EXIST", "info is not existed"); } return info; } |
단순히 Exception을 throw하면, ExceptionHandler가 Code와 Message를 json형태로 잘 매핑해서 응답하게 됩니다. (추가로 HttpStatusCode까지도 매핑하게 됩니다) 그래서 아래처럼 테스트를 하게 되면 오류가 발생하게 됩니다.
1 2 3 4 5 6 |
@Test public void findIfInfoIsNull() { givenNullIfSelect(); mockMvc.perform(put("/test")).andExpect(status().isBadRequest()); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Caused by: kr.co.freeim.test.exception.CustomException: info is not existed at kr.co.freeim.test.TestApiController.find(TestApiController.java:93) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:215) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:745) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:686) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:925) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:856) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:953) ... 38 more |
그렇다면 보통 아래처럼 테스트를 고치게 되겠죠?
1 2 3 4 5 6 |
@Test(expected = CustomException.java) public void findIfInfoIsNull() { givenNullIfSelect(); mockMvc.perform(put("/test")).andExpect(status().isBadRequest()); } |
그러고 나서 테스트를 돌리게 되면 또다른 오류가 발생합니다. 아… ExceptionHandler가 없어서 MockMvc에서 NestedServletException을 throw해버리네요..ㅠ.ㅠ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
java.lang.Exception: Unexpected exception, expected<kr.co.freeim.test.exception.CustomException> but was<org.springframework.web.util.NestedServletException> at org.junit.internal.runners.statements.ExpectException.evaluate(ExpectException.java:28) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) at org.junit.runners.ParentRunner.run(ParentRunner.java:309) at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37) at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62) at org.junit.runner.JUnitCore.run(JUnitCore.java:160) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140) |
일반적인 테스트에서 보면 MockMvc에 setHandlerExceptionResolvers()를 이용해서 ExceptionResolver를 셋팅합니다.
1 2 3 4 5 6 7 |
@Before public void setUp() throws Exception { mockMvc = MockMvcBuilders.standaloneSetup(dut) .setHandlerExceptionResolvers(new SimpleMappingExceptionResolver()) .alwaysDo(print()) .build(); } |
사실 ExceptionResolver를 implement한 SimpleMappingExceptionResolver라서 setHandlerExceptionResolvers()가 가능합니다. 그런데 위 코드는 ExceptionResolver의 구현체가 아닙니다.
1 2 3 4 5 6 7 8 9 10 |
@ControllerAdvice public class ApiExceptionHandler { @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ExceptionHandler({ CustomException.class }) @ResponseBody public RestError handleCustomException(HttpServletRequest request, Exception e) { log.info(String.format("handleException : remoteAddr=[%s], message=[%s]", request.getRemoteAddr(), e.getMessage()), e); return createError(e); } } |
@ControllerAdvice와 @ExceptionHandler를 이용한 사실상 더 최신식 기술입니다. 이거참 난감합니다. 테스트하려고 어노테이션까지 주입해야…ㅠ.ㅠ 그래서 구글을 찾아보던 중 아래와 같은 답을 발견했습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private static final StaticApplicationContext applicationContext = new StaticApplicationContext(); private static final WebMvcConfigurationSupport webMvcConfigurationSupport = new WebMvcConfigurationSupport(); private MockMvc mockMvc; @InjectMocks private TestController dut; @Before public void setUp() { applicationContext.registerSingleton("exceptionHandler", ApiControllerExceptionHandler.class); webMvcConfigurationSupport.setApplicationContext(applicationContext); mockMvc = MockMvcBuilders.standaloneSetup(dut) .setHandlerExceptionResolvers(webMvcConfigurationSupport.handlerExceptionResolver()) .alwaysDo(print()) .build(); } |
ApplicationContext를 만들어서 exceptionHandler로 등록을 한 다음에 handlerExceptionResolver()로 get을 하면 등록된 녀석들을 List<ExceptionHandler> 타입으로 리턴해줍니다. 결국 아래와 같이 최종 버전의 테스트가 정상적으로 수행됩니다.
1 2 3 4 5 6 7 8 |
@Test public void findIfInfoIsNull() { givenNullIfSelect(); mockMvc.perform(put("/test")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code", is("NOT_EXIST"))); } |
약간은 편법인 듯 하지만, 테스트하려고 하는 관심사가 아닌 부분이기 때문에 ApplicationContext를 로딩한다거나 해서 MvcTest가 무거워지는 것 보다는 훨씬 나은 선택이라 생각됩니다.