티스토리 뷰

1.테스트코드에서 자꾸 NPE가 나옴

2.productRepository.findById(requestDto.getProductId())이런 애들이 메서드에 감싸져 있어서 mock 객체를 제대로 참조하지 못한다고 판단함.
3.이런 메서드들을 override하고 필요에 따라 when()을 이용해서 낚아챌 필요가 있는 것들을 낚아채서 내가 설정한 값을 리턴하도록 해 둠

 

=================================================================================

미니프로젝트 진행 중 아래 코드의 테스트코드를 작성해야하는 필요가 생김.

 

//단일 상품 주문하기
@Transactional
public OrderDto orderOne(Members member, OrderRequestDto requestDto) {


    Cart cart = findCart(member);   //회원의 장바구니 가져오기
    Product product = checkProduct(requestDto); //상품정보
    checkStock(requestDto, product); //재고가 있는지 확인
    checkQuantity(requestDto);  //주문수량이 유효한지 확인

    //주문수량만큼 상품 테이블에 총 주문수량 증가시킨다
    product.setOrderCount(product.getOrderCount() + requestDto.getQuantity());

    //주문상품 객체 만들기(생성메서드)
    //주문 수량만큼 재고 차감
    OrderItem orderItem = OrderItem.createOrderItem(product, requestDto);
    List<OrderItem> orderItems = new ArrayList<>();
    orderItems.add(orderItem);

    //주문 객체 생성해서 회원정보와 주문상품 정보 저장
    Orders order = Orders.createOrder(member, orderItems);

    //주문정보 저장
    Orders savedOrders = orderRepository.save(order);

    deleteCartItem(cart, product); //장바구니에서 주문상품 삭제한다



    //주문내역 반환 (주문번호와, 주문일자)
    return OrderDto.builder()
            .orderId(savedOrders.getId())
            .orderDate(savedOrders.getCreatedAt())
            .resultStock(product.getStock())
            .build();
}

 

일단 단위테스트니까 @SpringBootTest를 써서 어플리케이션을 실제로 동작시키면서 할 필요가 없다고 생각함.
mock객체를 만들어서 테스트 할거니까 mockito 사용을 위해 클래스에 @ExtendWith(MockitoExtension.class) 이 어노테이션만 달아주었다.

 

@ExtendWith(MockitoExtension.class)


처음에는 이런 식으로 필요한 객체들을 만들었음.
member나 cart같은 것들은 다른 테스트코드에서도 사용할 것 같아 전역에서 사용할 수 있게 빼 두었음.

 

@Mock
private OrderRepository orderRepository;

@Mock
private CartRepository cartRepository;

@Mock
private ProductRepository productRepository;

@Mock
private MembersRepository membersRepository;
@Mock
private CartItemRepository cartItemRepository;

@Mock
private RedissonClient redissonClient;

Members member;
Cart cart;

Product product;
CartItem cartItem;
OrderRequestDto orderRequestDto;

@BeforeEach
void beforeEach() {
    member = new Members("user", "1234", "test@test.com", "test", MembersRoleEnum.MEMBER);
    cart = new Cart(member);
    product = new Product(1L, 30, "testName", 16000, 50,0L){

    };
    cartItem = new CartItem(cart, product);

    orderRequestDto = new OrderRequestDto();
    orderRequestDto.setProductId(1L);
    orderRequestDto.setQuantity(10);
    orderRequestDto.setDiscount(1000f);
    orderRequestDto.setDcType("none");
}


일단 가볍게 given when then을 작성해봄.
given의 경우 beforeEach에서 주어졌다고 생각하고 따로 작성하지 않았음.

 

@Test
@DisplayName("단일 상품 주문")
void orderOne() {
    //given

    //when
    orderService.orderOne(member, orderRequestDto);

    //then
    //재고가 차감되었는지 확인
    Assertions.assertEquals(20, product.getStock());
}

 

테스트 하려는 코드의 이 부분에서 NPE가 나옴.

 

Product product = checkProduct(requestDto); //상품정보


checkProduct라는 메서드가 문제였음. mock을 사용하다보니 실제로 저장이 되거나 하지는 않는 게 문제였음.

 

//상품이 존재하는지 확인
private Product checkProduct(OrderRequestDto requestDto) {
  return productRepository.findById(requestDto.getProductId()).orElseThrow(
            () -> new IllegalArgumentException("상품이 존재하지 않습니다")
    );
}

 

그럼 when()을 이용해서 productRepository.findById()이거를 낚아채면 되겠다 싶었음.

@Test
@DisplayName("단일 상품 주문")
void orderOne() {
    //given
	when(productRepository.findById(orderRequestDto.getProductId()))
		.thenReturn(Optional.ofNullable(product));
        
    //when
    orderService.orderOne(member, orderRequestDto);

    //then
    //재고가 차감되었는지 확인
    Assertions.assertEquals(20, product.getStock());
}


그런데도 같은 문제가 발생함.
아무래도 productRepository.findById()이 쿼리가 checkProduct라는 메서드로 감싸져 있는 게 문제인 것 같았음.
orderService 내부에서 작동하다보니 mock 객체를 제대로 참조하지 못한다고 판단함.

어떻게 하면 될지 생각을 하다가 override를 하면 되겠다 라는 생각을 함.

그런데 checkProduct 메서드의 접근제어자가 private이네? 이러면 override가 안되니 protected로 바꿔주었음.

default를 쓸까 protected를 쓸까 고민했는데, 조금 더 포괄적인 protected를 써 보았다.
사실 서브클래스가 사용되고 있지 않으니, default를 쓰는 게 맞다고 생각되었는데 혹시나 여지가 있을까 싶어 protected를 썼다.

 

//상품이 존재하는지 확인
protected Product checkProduct(OrderRequestDto requestDto) {
  return productRepository.findById(requestDto.getProductId()).orElseThrow(
            () -> new IllegalArgumentException("상품이 존재하지 않습니다")
    );
}


결과적으로 orderService 내부에서 따로 메서드로 만들어둔 checkProduct같은 것들이 mock을 참조하지 못하게 되면서 NPE를 뱉어냈기 때문에, 재정의가 필요한 것들을 찾아서 미리 재정의를 하고, 따로 낚아챌 메서드는 when()을 이용해서 낚아챘다.

또한 원래 코드에서 회원의 장바구니를 가져와 장바구니를 한 번 비우는 로직이 있는데, 이게 잘 작동하는지 확인을 위해서
내가 테스트를 위해 만든 cart 객체가 불러와져야 했다.

원래 override를 하는 이유가 메서드에 감싸져 있어 mock 참조가 제대로 안되었기 때문이니 그냥 코드 자체는 똑같아도 상관 없었는데,

어짜피 테스트코드에서는 내가 미리 설정한 객체가 불러와지기만 하면 되니까 내가 설정한 객체를 리턴하도록 수정하였다.

결과적으로 이렇게 테스트 코드를 작성하였다.

 


@ExtendWith(MockitoExtension.class)
@Slf4j
@DisplayName("orderService 테스트")
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private CartRepository cartRepository;

    @Mock
    private ProductRepository productRepository;

    @Mock
    private MembersRepository membersRepository;
    @Mock
    private CartItemRepository cartItemRepository;

    @Mock
    private RedissonClient redissonClient;

    Members member;
    Cart cart;

    Product product;
    CartItem cartItem;
    OrderRequestDto orderRequestDto;

    @BeforeEach
    void beforeEach() {
        member = new Members("user", "1234", "test@test.com", "test", MembersRoleEnum.MEMBER);
        cart = new Cart(member);
        product = new Product(1L, 30, "testName", 16000, 50,0L){

        };
        cartItem = new CartItem(cart, product);

        orderRequestDto = new OrderRequestDto();
        orderRequestDto.setProductId(1L);
        orderRequestDto.setQuantity(10);
        orderRequestDto.setDiscount(1000f);
        orderRequestDto.setDcType("none");
    }



    @Nested
    @DisplayName("성공 케이스")
    class successCase {

        @Test
        @DisplayName("단일 상품 주문")
        void orderOne() {
            //given

            //orderRepository.save()메서드가 호출되면 가로채서 save()의 0번째 인자를 리턴하도록 함.
            when(orderRepository.save(any(Orders.class))).thenAnswer(invocation -> invocation.getArgument(0));

            //회원의 장바구니를 찾을 때 내가 미리 만들어둔 cart를 리턴해주기 위해
            Map<Members, Cart> testCartMap = new HashMap<>();
            testCartMap.put(member, cart);
            OrderService orderService = new OrderService(orderRepository, productRepository, cartItemRepository, cartRepository, redissonClient) {

                //findCart라는 메서드가 사용되면 내가 지정한 cart를 바로 가져오게 하기 위해
                //그냥 원래 service에 있던 코드를 그대로 가져와도 되는데, 그러면 when으로 가로채야 하는 코드가 더 생겨서 이렇게 작성함
                @Override
                protected Cart findCart(Members members) {
                    return testCartMap.get(member);
                }

                @Override
                protected Product checkProduct(OrderRequestDto requestDto) {
                    return product;
                }

                //service에 있는 대로 그대로 복사. override를 안하면 mock객체를 보는 게 아니어서 null값을 리턴함.
                @Override
                protected void checkStock(OrderRequestDto orderRequestDto, Product product) {
                    if (orderRequestDto == null) {
                        throw new IllegalArgumentException("주문 정보가 존재하지 않습니다.");
                    }
                    if (product == null) {
                        throw new IllegalArgumentException("상품 정보가 존재하지 않습니다.");
                    }
                    if (orderRequestDto.getQuantity() > product.getStock()) {
                        throw new IllegalArgumentException("주문 가능 수량을 초과하였습니다");
                    }
                }

                @Override
                protected void checkQuantity(OrderRequestDto requestDto) {
                    if (requestDto.getQuantity() < 1) {
                        throw new IllegalArgumentException("최소 1개 이상 주문해주세요");
                    }
                }
            };

            //when
            orderService.orderOne(member, orderRequestDto);

            //then
            //재고가 차감되었는지 확인
            Assertions.assertEquals(20, product.getStock());
            //장바구니가 다 지워졌는지
            Assertions.assertEquals(0,cartItemRepository.findByCartAndProduct(cart,product).stream().count());



        }
댓글