E2E 테스트를 작성하면서 이 로직을 테스트(검증)하는 것 외 얻었던 장점이 있었다. 바로 테스트 코드가 문서화 역할을 할 수 있다는 것. Cypress Command를 활용한 선언적 테스트 작성은 이 컴포넌트가 어떤 역할을 하는지, 이 기능이 어떤 기능을 가지고 있는지 설명 할 수 있는 가이드라인이 됐다.

이번에도 React-Testing-Library를 사용하며 테스트를 작성하는데 선언적인 테스트 작성으로 이 컴포넌트의 가이드라인을 제공해주고 싶었다. 그래서 아래와 같이 고도화된 render 함수를 사용했다.

describe('<CartListItem />', () => {
  const cartItem = createCartItem();
  const carts = [createCartItem(0), createCartItem(1)];

  const VALID_CART_ITEM_QUANTITY = 10;
  const MIN_CART_ITEM_QUANTITY = 1;
  const MAX_CART_ITEM_QUANTITY = 20;

  const cartItemHasValidQuantity = 
         createCartItem(1, VALID_CART_ITEM_QUANTITY);
  const cartItemHasMinQuantity = 
         createCartItem(1, MIN_CART_ITEM_QUANTITY);
  const cartItemHasMaxQuantity = 
         createCartItem(1, MAX_CART_ITEM_QUANTITY);

  beforeEach(() => {
    jest.clearAllMocks();

    (useDispatch as jest.Mock).mockImplementation(() => useAppDispatch);
    (useSelector as jest.Mock).mockImplementation(() => useAppSelector);
  });

  it('장바구니에 담긴 아이템 정보를 보여준다.', () => {
    const { ProductName, ProductPrice, CartItemQuantity, IncreaseQuantityButton, DecreaseQuantityButton } =
      renderCartListItem(cartItem, carts);

    expect(ProductName()).toBeInTheDocument();
    expect(ProductPrice()).toBeInTheDocument();
    expect(CartItemQuantity()).toBeInTheDocument();
    expect(IncreaseQuantityButton()).toBeInTheDocument();
    expect(DecreaseQuantityButton()).toBeInTheDocument();
  });

  it('장바구니 수량이 20개 미만 일때 버튼(+)을 누르면 액션이 dispatch 된다.', async () => {
    const { clickIncreaseQuantityButton, calledDispatch } = renderCartListItem(cartItemHasValidQuantity, carts);

    clickIncreaseQuantityButton();
    calledDispatch();
  });

  it('장바구니 수량이 20개인 경우 버튼(+)을 누르면 액션이 dispatch 되지 않는다.', async () => {
    const { clickIncreaseQuantityButton, notCalledDispatch } = renderCartListItem(cartItemHasMaxQuantity, carts);

    clickIncreaseQuantityButton();
    notCalledDispatch();
  });

  it('장바구니 수량이 1개를 초과하는 경우 버튼(-)을 누르면 액션이 dispatch 된다.', () => {
    const { clickDecreaseQuantityButton, calledDispatch } = renderCartListItem(cartItemHasValidQuantity, carts);

    clickDecreaseQuantityButton();
    calledDispatch();
  });

  it('장바구니 수량이 1개인 경우 버튼(-)을 누르면 액션이 dispatch 되지 않는다.', async () => {
    const { clickDecreaseQuantityButton, notCalledDispatch } = renderCartListItem(cartItemHasMinQuantity, carts);

    clickDecreaseQuantityButton();
    notCalledDispatch();
  });
});

이 함수로 얻을 수 있었던 점은 크게 두개였던 것 같다.

  1. button을 찾는 로직을 테스트 내부에서 제거시킬 수 있다. → 예를들어 result.getByText(’담기버튼') 와 같은 로직이 담기지 않음. 그냥 expect(AddCartButton()).toBeInTheDocument() 와 같이 작업 할 수 있음
  2. 반복되는 로직을 제거 할 수 있음. → 계속 getByText(), getByRole 하는 작업을 줄여줄 수 있음

위와 같은 장점을 가질 수 있는 render 함수로 아래와 같이 테스트를 작성했다.

describe('<CartListItem />', () => {
  const cartItem = createCartItem();
  const carts = [createCartItem(0), createCartItem(1)];

  const VALID_CART_ITEM_QUANTITY = 10;
  const MIN_CART_ITEM_QUANTITY = 1;
  const MAX_CART_ITEM_QUANTITY = 20;

  const cartItemHasValidQuantity = createCartItem(1, VALID_CART_ITEM_QUANTITY);
  const cartItemHasMinQuantity = createCartItem(1, MIN_CART_ITEM_QUANTITY);
  const cartItemHasMaxQuantity = createCartItem(1, MAX_CART_ITEM_QUANTITY);

  beforeEach(() => {
    jest.clearAllMocks();

    (useDispatch as jest.Mock).mockImplementation(() => useAppDispatch);
    (useSelector as jest.Mock).mockImplementation(() => useAppSelector);
  });

  it('장바구니에 담긴 아이템 정보를 보여준다.', () => {
    const { ProductName, ProductPrice, CartItemQuantity, IncreaseQuantityButton, DecreaseQuantityButton } =
      renderCartListItem(cartItem, carts);

    expect(ProductName()).toBeInTheDocument();
    expect(ProductPrice()).toBeInTheDocument();
    expect(CartItemQuantity()).toBeInTheDocument();
    expect(IncreaseQuantityButton()).toBeInTheDocument();
    expect(DecreaseQuantityButton()).toBeInTheDocument();
  });

  it('장바구니 수량이 20개 미만 일때 버튼(+)을 누르면 액션이 dispatch 된다.', async () => {
    const { clickIncreaseQuantityButton, calledDispatch } = renderCartListItem(cartItemHasValidQuantity, carts);

    clickIncreaseQuantityButton();
    calledDispatch();
  });

  it('장바구니 수량이 20개인 경우 버튼(+)을 누르면 액션이 dispatch 되지 않는다.', async () => {
    const { clickIncreaseQuantityButton, notCalledDispatch } = renderCartListItem(cartItemHasMaxQuantity, carts);

    clickIncreaseQuantityButton();
    notCalledDispatch();
  });

  it('장바구니 수량이 1개를 초과하는 경우 버튼(-)을 누르면 액션이 dispatch 된다.', () => {
    const { clickDecreaseQuantityButton, calledDispatch } = renderCartListItem(cartItemHasValidQuantity, carts);

    clickDecreaseQuantityButton();
    calledDispatch();
  });

  it('장바구니 수량이 1개인 경우 버튼(-)을 누르면 액션이 dispatch 되지 않는다.', async () => {
    const { clickDecreaseQuantityButton, notCalledDispatch } = renderCartListItem(cartItemHasMinQuantity, carts);

    clickDecreaseQuantityButton();
    notCalledDispatch();
  });
});

확실히 고도화된 render 함수를 사용하기 전 보다 테스트가 선언적이게 되었고, 기능을 어느 정도 더 보기 쉽게 해주는 것 같다. 크게 비교해보면 전과 후는 이렇게 다르다.

수정 전

it('장바구니 수량이 1개인 경우 버튼(-)을 누르면 액션이 dispatch 되지 않는다.', async () => {
  const onClickCartItemSelectButton = jest.fn();
  const result = render(
    <CartListItem
      cartItem={cartItemHasMinQuantity}
      onClickCartItemSelectButton={onClickCartItemSelectButton}
      selectedCartItems={carts}
    />,
  );

  userEvent.click(result.getByText('+'));
  expect(useDispatch).not.toBeCalled();
});

수정 후

 it('장바구니 수량이 1개인 경우 버튼(-)을 누르면 액션이 dispatch 되지 않는다.', async () => {
    const { clickDecreaseQuantityButton, notCalledDispatch } 
                    = renderCartListItem(cartItemHasMinQuantity, carts);

    clickDecreaseQuantityButton();
    notCalledDispatch();
  });
});

확실히 정말 어떤 것을 테스트하는 것인지 테스트 내역만 보고도 이해 할 수 있다. 분명 좋은 것 같은데 그럼에도 불구하고 아래와 같은 의문도 든다..

고도화된 렌더 함수로 선언적인 테스트를 작성하며 드는 의문

<aside> 💡 테스트는 로직을 검증하기 위한 도구라고 생각 할 수 있는데 그 안에 또 다시 로직을 담는 것이 맞을까?

</aside>