본문 바로가기
📕 Spring Framework/Spring Project

「컨트롤러 단위 테스트」

by GroovyArea 2022. 6. 23.

https://github.com/GroovyArea/MyChickenBreastShop

 

GitHub - GroovyArea/MyChickenBreastShop: ChikenBreastShop API with Spring boot

ChikenBreastShop API with Spring boot. Contribute to GroovyArea/MyChickenBreastShop development by creating an account on GitHub.

github.com

저번 주에 처음으로 단위 테스트를 공부하고 적용시키며 Spring REST Docs를 이용해 적용시켰다. 
처음 단위 테스트를 작성하다 보니 미흡한 부분이 좀 있었다. 그 부분을 수정해가며 다른 컨트롤러의 단위 테스트를 추가로 작성했다.
API 문서를 작성할 때도 통합해서 작성 중이었는데, 도메인 별로 나누어 수정하며 작성해보았다.
컨트롤러를 마치게 되면 서비스 계층의 단위 테스트도 의미가 있을 경우 작성해 볼 생각이다.

테스트 코드 작성이 완료 되면 성능 측정을 하며, 리팩터링을 할 계획도 수립했다.

토비의 스프링 책은 거의 다 읽어 가는데, 정말 좋은 책 같다. 스프링을 제대로 공부하는 사람에게 무조건 추천하고 싶은 서적이다. 어렵지만 정확하게 이해하며 마무리하고 얼른 JPA를 공부하며 현재 프로젝트에 적용시켜보고 싶다. 

 

장바구니 컨트롤러 단위 테스트

저번 주에 회원, 상품 컨트롤러의 단위 테스트를 진행했다. 이번 3일간 진행한 단위 테스트는 기존 테스트 코드의 문제점을 보완해 진행했다.

 

@BeforeEach
void setUpCookie() {
    cartItemDTOMap.put(addCartDTO1.getProductNo(), addCartDTO1);
    cartItemDTOMap.put(addCartDTO2.getProductNo(), addCartDTO2);

    cookieList.add(cookie1);
    cookieList.add(cookie2);
    cookieList.add(cookie3);
    cookieList.add(cookie4);

    newCartCookie = new Cookie("Chicken", URLEncoder.encode(JsonUtil.objectToString(cartItemDTOMap), StandardCharsets.UTF_8));
    newCartCookie.setMaxAge(60 * 60 * 24 * 7);
    newCartCookie.setPath("/api");

    cookieList.add(newCartCookie);
    cookies = cookieList.toArray(Cookie[]::new);
}

테스트를 실행할 때마다 기본적으로 쿠키 값을 세팅해야 하기 때문에 @BeforeEach를 사용해 Setup 메서드를 만들었다.

 

@Test
@DisplayName("장바구니 조회 테스트")
void getCartListTest() throws Exception {
    Mockito.when(cartService.getCartCookie(cookies)).thenReturn(newCartCookie);
    Mockito.when(cartService.getCartDTOMap(newCartCookie)).thenReturn(cartItemDTOMap);

    mockMvc.perform(get("/api/carts")
                    .cookie(cookies))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0]").exists())
            .andExpect(jsonPath("$[1]").exists())
            .andDo(print());
}

기본적인 장바구니 조회 테스트이다. @Mockbean 된 서비스 메서드의 동작을 먼저 정의하고(given) 결과 값의 존재 유무로 판단했다(when & then).

 

@Test
@DisplayName("장바구니 상품 삭제 테스트")
void deleteCart() throws Exception {
    cartItemDTOMap.remove(deleteCartDTO.getProductNo());

    cookieList.add(newCartCookie);

    Mockito.when(cartService.getCartCookie(cookies)).thenReturn(newCartCookie);
    Mockito.when(cartService.getCartDTOMap(newCartCookie)).thenReturn(cartItemDTOMap);
    Mockito.when(cartService.resetCartCookie(newCartCookie, cartItemDTOMap)).thenReturn(newCartCookie);

    mockMvc.perform(delete("/api/carts")
                    .cookie(cookies)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(JsonUtil.objectToString(deleteCartDTO)))
            .andExpect(status().isOk())
            .andExpect(content().string(ResponseMessage.DELETE_MESSAGE.getValue()))
            .andDo(print());
}

나머지 테스트는 거의 비슷하기 때문에 삭제 테스트도 거의 비슷한 구현이다. 

 

테스트를 수행 후 생성된 snippets 파일들로 명세를 만들었다. 본 그림은 도메인 별로 나눈 모습이다.

 

주문 컨트롤러 단위 테스트

주문 컨트롤러는 카카오페이 REST API를 이용해 구현했었다. 그래서 넘겨받고 줄 파라미터들이 엄청 많다. 

그래서 애 좀 먹었다. 그리고 mockito의 개념 정립에 확실히 더 다져간 테스트이기도 하다.

 

@Test
@DisplayName("회원 주문 조회 리스트 테스트")
void getDBOrderInfo() throws Exception {
    List<OrderInfoDTO> orderInfoDTOList = new ArrayList<>();
    orderInfoDTOList.add(orderInfoDTO);
    Mockito.when(orderService.getOrderInfoList(orderInfoDTO.getPartnerUserId())).thenReturn(orderInfoDTOList);

    mockMvc.perform(get("/api/order/{userId}", orderInfoDTO.getPartnerUserId())
                    .header("Authorization", "Bearer ${ADMIN_AUTH_TOKEN}"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.data").exists())
            .andExpect(jsonPath("$.message", is("회원 주문 조회 리스트입니다.")))
            .andDo(print());
}

DB에 저장된 주문 완료 데이터를 조회하는 아주 기본적인 테스트이다.

 

@Test
@DisplayName("단건 결제 테스트")
void orderAction() throws Exception {

    String userId = orderInfoDTO.getPartnerUserId();
    String url = "https://online-pay.kakao.com/mockup/v1/cd749f82c8c58decb5c832ab45a0990e02c87b07e32bf3f28aa9b9297a0cf710/info";

    Mockito.when(kakaoPayService.getkakaoPayUrl(any(OrderProductDTO.class), anyString(), anyString())).thenReturn(url);

    mockMvc.perform(post("/api/order")
                    .header("Authorization", "Bearer ${ADMIN_AUTH_TOKEN}")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(JsonUtil.objectToString(orderProductDTO))
                    .requestAttr("tokenUserId", userId))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.data", is(url)))
            .andExpect(jsonPath("$.message", is("카카오 페이 결제 URL")))
            .andDo(print());
}

 

@Test
@DisplayName("장바구니 결제 테스트")
void cartOrderAction() throws Exception {
    given(cartService.getCartCookie(cookies)).willReturn(newCartCookie);

    String userId = orderInfoDTO.getPartnerUserId();
    String url = "https://online-pay.kakao.com/mockup/v1/ce3cb801e74a337fb7cf74d9a9aa139bb569e74ba316ae7eb282b90f8f08a522/info";

    given(kakaoPayService.getCartKakaoPayUrl(any(Integer[].class), any(String[].class),
            any(Integer[].class), anyInt(), anyString(), anyString())).willReturn(url);

    mockMvc.perform(post("/api/order/cart")
                    .header("Authorization", "Bearer ${ADMIN_AUTH_TOKEN}")
                    .cookie(cookies)
                    .requestAttr("tokenUserId", userId))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.data", is(url)))
            .andExpect(jsonPath("$.message", is("카카오 페이 결제 URL")))
            .andDo(print());
}

여기서 문제가 있었다. Mockbean 정의한 서비스 메서드의 파라미터를 여러개 넘겨주는 상황을 given으로 주어야 하는데, 구체적인 변수를 정의하여 넣으니 계속 메서드가 null을 return 했다. 

 

어디가 잘못되었나 계속 보았고, 검색을 한참 해보니, mockito는 기본적으로 mockbean 인스턴스는 호출된 json을 역직렬 화하여 새롭게 빌드된 인스턴스이므로 기존 인스턴스의 지시에 응답하지 않는다고 한다. 그래서

any() 메서드를 이용하여 정상적으로 값을 얻는 결과를 내는 테스트를 성공적으로 완료하게 되었다.

 

참조 : https://stackoverflow.com/questions/55498142/mocked-method-return-value-is-null-in-spring-boot-webmvctest

 

Mocked method return value is null in Spring Boot @WebMvcTest

The below test fails with: org.json.JSONException: Unparsable JSON string. I tried various variations of the mocking the saveAndFlush method // doAnswer(returnsFirstArg()).when(this.

stackoverflow.com

 

추후 진행할 단위 테스트는 결제 컨트롤러 단위 테스트가 남았다. 부지런히 작성해보자~~

 

금주 계획

  1. 토비의 스프링 완독 (오늘 완독해보자)
  2. JPA 공부 시작 (오늘 살까..? 내일 사자)
  3. 서비스 계층 단위 테스트 & 성능 측정 (APM 테스트)
  4. 스케쥴러 실험 (예외 롤백 처리 테스트)
  5. 자료구조 정리 (내일) 및 해당 문제 3개 풀기
  6. http, 운영체제 자기 전 틈틈이 읽기
반응형