새로 작업한 매니저 사이트에 예외처리를 구성하던 중 ControllerAdvice
를 사용하게 되었습니다.
ControllerAdvice 를 테스트하기 어려운 어려운 이유는 단순히 Junit 에서 제공하는 excpeted
를 사용하여 테스트할 수 없기 때문입니다.
Junit 에서 제공하는 @Test 의 expected 는 테스트하고자하는 서비스가 예외를 던졌을 때 던져진 예외가 어떤 Class 인지를 확인하기 위한 용도
로 사용됩니다.
예를 들어, 아래와 같은 Login 시 조회된 결과가 존재하지 않을 때 NotExistUserException 을 던지는 Service 가 존재한다고 가정합니다.
public List<LoginEntity.Login> login(LoginVO.Login params){
List<LoginEntity.Login> users = loginMapper.login(params);
if (empty(results)) {
throw new NotExistUserException("조회된 계정이 존재하지 않습니다.");
}
return users;
}
이에 대한 예외를 테스트하기 위해서는 아래와 같이 존재하지 않는 User 를 paramter 로 던지도록 하여 예외를 던지도록 한 후 @Test 의 expected 에 던져진 예외를 정의하여 테스트합니다.
@Test(expected = NotExistUserException.class)
public void 로그인시_조회된_유저가_없을시_NotExistUserException_예외를_던지는지() {
//given
LoginVo.Login user = notExistUser();
//when
loginService.login(user);
}
하지만, @ControllerAdvice 는 NotExsitUserException 의 예외가 던져진 후 예외에 해당하는 특정 로직을 처리하는지를 검증해야할 필요가 있기때문에 위에 같이 로직을 처리한다면 검증 실패 오류가 발생하게 됩니다.
따라서 ControllerAdvice 를 테스트하기 위해서는 Acceptance Test 와 유사하게 HTTP 통신을 진행한 뒤 해당 통신에 대한 예외처리 후의 Header 와 Body 값을 검증하는 방식으로 테스트할 수 있습니다.
@ControllerAdvice
public class BusinessExceptionControllerAdvice {
@ExceptionHandler(NotExistUserException.class)
public ResponseEntity<?> handleNotExistUserException(NotExistUserException e) {
BaseResult.Normal result = new BaseResult.Normal();
printExceptionMessage(e.getMessage());
return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED);
}
}
위와 같이 @Controller 및 @RestController 를 가지고 있는 Controller 에서 NotExsistUserException 을 던져 위와 같이 ControllerAdvice 에서 횡적으로 받아 로그를 찍은 후 UNAUTHORIZED HTTP 상태값을 헤더값을 담아서 예외를 처리하는 로직이 있다고 가정하도록 하겠습니다.
위의 코드를 검증하기 위해 아래와 같은 테스트코드를 작성하였습니다.
@Test
public void 로그인한_값에_대한_user_가_존재하지_않을때_ControllerAdvice_에서_NotExistUserException_예외를_처리하는지() throws Exception {
//given
String request = getJsonStringByVo(notExistUser());
//when
when(userController.login(any())).thenThrow(new NotExistListException("존재하지 않는 계정입니다."));
//then
mockMvc.perform(post("/user/login").contentType(APPLICATION_JSON_UTF8)
.content(request))
.andExpect(status().isUnauthorized())
.andExpect(result ->
assertThat(getApiResultExceptionClass(result)).isEqualTo(NotExistListException.class)
);
}
private String getJsonStringByVo(Object parameter) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String json = new ObjectMapper().writeValueAsString(parameter);
return mapper.readValue(json, new TypeReference<Map<String, Object>>(){});
}
private Class<? extends Exception> getApiResultExceptionClass(MvcResult result) {
return Objects.requireNonNull(result.getResolvedException()).getClass();
}
위의 코드는 ControllerAdvice 에 대한 테스트 검증 코드입니다.
천천히 살펴보도록 하겠습니다.
String request = getJsonStringByVo(notExistUser());
- 우선 mockMvc 로 테스트하고자하는 Api Controller 에게 request 를 요청하기 위한 request 데이터를 준비합니다.
when(userController.login(any())).thenThrow(new NotExistListException("존재하지 않는 계정입니다."));
- Test 를 하고자하는 Controller 가 NotExistListException 예외를 던지도록 하여 테스트할 수 있도록 Mockito 를 통해 가정을 해줍니다.
mockMvc.perform(post("/user/login").contentType(APPLICATION_JSON_UTF8) .content(request)) .andExpect(status().isUnauthorized()) .andExpect(result -> assertThat(getApiResultExceptionClass(result)).isEqualTo(NotExistListException.class) ); }
- mockMvc 를 이용하여 HTTP Post 통신을
DispatcherServlet
에 요청을 보내 호출합니다. MockMvcRequestBuilders
을 사용하여 설정한 요청 데이터를 perform() 의 인수로 전달합니다.- perform() 에서 반환된
ResultAction()
을 호출하여 결과값을 검증합니다. - status() 메서드를 통해 Header 에 존재하는 HttpStatus 값이 UNAUTHORIZED 인지 검증합니다.
getResolvedException()
를 통해 Http 통신간에 exception 이 존재하는 경우 예외에 해당하는 class 를 가져와 검증합니다.
- mockMvc 를 이용하여 HTTP Post 통신을
https://itmore.tistory.com/entry/MockMvc-%EC%83%81%EC%84%B8%EC%84%A4%EB%AA%85 에 MockMvc 에 대한 자세한 설명이 정리되어 있습니다.
테스트 코드를 빌드하면 해당하는 예외처리에 대한 응답 로그와 값이 출력되면서 테스트가 성공하게 됩니다.
만약 해당 URL 에 대응되는 API 가 존재하지 않는다면 다음과 같은 404 오류가 발생합니다.
java.lang.AssertionError: Status
Expected :200
Actual :404
<Click to see difference>
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:55)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:82)
at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$matcher$9(StatusResultMatchers.java:619)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:195)
at com.paymint.m2.api.manager.exception.BusinessExceptionControllerAdviceTest.로그인한_값에_대한_user_가_존재하지_않을때_ControllerAdvice_에서_NotExistUserException_예외를_처리하는지(BusinessExceptionControllerAdviceTest.java:86)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.mockito.internal.runners.DefaultInternalRunner$1.run(DefaultInternalRunner.java:79)
at org.mockito.internal.runners.DefaultInternalRunner.run(DefaultInternalRunner.java:85)
at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:39)
at org.mockito.junit.MockitoJUnitRunner.run(MockitoJUnitRunner.java:163)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
테스트하고자하는 API 의 URL 과 perform 에 작성된 urlTemplate
변수만 잘 맞춰준다면 해결할 수 있습니다.
테스트코드를 초반에 공부할 때에는 예외처리에 대한 테스트코드의 중요성을 느끼지 못하여 별로 작성하지 않았었습니다.
하지만, 실제 서비스를 운영하고 보니 가장 문제가 된 것이 예외처리에 대한 테스트코드 부족
이었습니다.
그 이유는 실제로 사용자로 인한 에상치 못한 오류가 많이 발생하는데, 이에 대한 예외처리와 그에 따른 테스트 코드가 충분하지 못한다면 추후에도 똑같은 상황에 대해서 대처하지 못할 수 있기 떄문입니다.
따라서, 예외처리에 대한 테스트코드는 로직에 대한 테스트코드와 같이 필수로 작성되어줘야 합니다.
'TEST CODE' 카테고리의 다른 글
[JUnit]같은 타입인 여러개의 Mock 객체를 @InjectMock 으로 주입 시 파라미터를 제대로 인식하지 못하는 문제 해결 (0) | 2019.12.16 |
---|---|
[JUnit] Test code 작성시 DI(Dependencies Inject) 를 적용하는 방법 (0) | 2019.11.27 |
[Junit] JunitSoftAssertion 으로 중복 검증 문제 해결하기 (0) | 2019.10.03 |
[Junit] Test Code 와 TDD (0) | 2019.08.17 |