본문 바로가기

TEST CODE

[Junit] Spring boot 에서 @ControllerAdvice 를 테스트하는 방법

 

 

새로 작업한 매니저 사이트에 예외처리를 구성하던 중 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 에 대한 테스트 검증 코드입니다.

천천히 살펴보도록 하겠습니다.

 

  1. String request = getJsonStringByVo(notExistUser());
    • 우선 mockMvc 로 테스트하고자하는 Api Controller 에게 request 를 요청하기 위한 request 데이터를 준비합니다.
  2. when(userController.login(any())).thenThrow(new NotExistListException("존재하지 않는 계정입니다."));
    • Test 를 하고자하는 Controller 가 NotExistListException 예외를 던지도록 하여 테스트할 수 있도록 Mockito 를 통해 가정을 해줍니다.
  3. 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 를 가져와 검증합니다.

 

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 변수만 잘 맞춰준다면 해결할 수 있습니다.

 

 

테스트코드를 초반에 공부할 때에는 예외처리에 대한 테스트코드의 중요성을 느끼지 못하여 별로 작성하지 않았었습니다.

하지만, 실제 서비스를 운영하고 보니 가장 문제가 된 것이 예외처리에 대한 테스트코드 부족이었습니다.

 

그 이유는 실제로 사용자로 인한 에상치 못한 오류가 많이 발생하는데, 이에 대한 예외처리와 그에 따른 테스트 코드가 충분하지 못한다면 추후에도 똑같은 상황에 대해서 대처하지 못할 수 있기 떄문입니다.

따라서, 예외처리에 대한 테스트코드는 로직에 대한 테스트코드와 같이 필수로 작성되어줘야 합니다.

 

 

 

MockMvc 상세설명

스프링 MVC 테스트 스프링 MVC 컨트롤러의 테스트 컨트롤러의 주요역할은 다양 컨트롤러의 주요역할 요청 경로 처리내용의 매핑 입력값 검사 요청한 데이터의 취득 비즈니스 로직 호출 다음 이동 화면의 제어 정작..

itmore.tistory.com