RTL ๋น„๋™๊ธฐ ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•œ ๋…ธ์ถœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

date
2025-10-20
order
6

RTL ๋น„๋™๊ธฐ ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•œ ๋…ธ์ถœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

์ด์ œ ProductList ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” api(useProducts)๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•ด์„œ ํ™”๋ฉด์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋ง ๋˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

//ProductList.spec.jsx
it('๋กœ๋”ฉ์ด ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ๋ชจ๋‘ ๋…ธ์ถœ๋œ๋‹ค', async () => {
  await render(<ProductList limit={PRODUCT_PAGE_LIMIT} />);

  const productCards = screen.getByTestId('product-card'); // ์‚ฌ์ „์— ์ •์˜ํ•จ

  expect(productCards).toHaveLength(PRODUCT_PAGE_LIMIT);

  productCards.forEach((el, index) => {
    const productCard = within(el);
    const product = data.products[index];

    expect(productCard.getByText(product.title)).toBeInTheDocument();
    expect(productCard.getByText(product.category.name)).toBeInTheDocument();
    expect(
      productCard.getByText(formatPrice(product.price)),
    ).toBeInTheDocument();
    expect(
      productCard.getByRole('button', { name: '์žฅ๋ฐ”๊ตฌ๋‹ˆ' }),
    ).toBeInTheDocument();
    expect(
      productCard.getByRole('button', { name: '๊ตฌ๋งค' }),
    ).toBeInTheDocument();
  });
});

getByTestId : ํ”„๋กœ๋•ํŠธ์นด๋“œ ์ปดํฌ๋„ŒํŠธ๋Š” ์ง€๊ธˆ๊นŒ์ง€ ์‚ฌ์šฉํ•ด์˜จ ๋งค์ฒ˜ getByText๋‚˜ getByRole๋ฅผ ํ†ตํ•ด ํ™•์ธํ•˜๊ธฐ ํž˜๋“ค๋‹ค. ๋ฏธ๋ฆฌ ProductCard์ปดํฌ๋„ŒํŠธ์˜ ํ…Œ์ŠคํŠธ ์•„์ด๋””๋ฅผ ์ง€์ •ํ•ด์„œ ๊ทธ๊ฒƒ์œผ๋กœ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ์˜ ์†์„ฑ์— ์ด๋ ‡๊ฒŒ ์ถ”๊ฐ€ํ•ด์„œ ์ •์˜ํ–ˆ

//ProductCard.jsx
//...
  return (
    <Grid
      item
      xs={6}
      sm={6}
      md={3}
      onClick={handleClickItem}
      data-testid="product-card" // <- ์ด๊ฒƒ์œผ๋กœ ์ฐพ์•„๋‚ธ๋‹ค
    >

๊ทธ๋Ÿฐ๋ฐ ์œ„์˜ ํ…Œ์ŠคํŠธ์—๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”๋ฐ, getByTestId ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด ๋น„๋™๊ธฐ(Promise) ์ฝ”๋“œ๋Š” ์‹คํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค.

findby... ํ•จ์ˆ˜

๊ทธ๋Ÿฌ๋‚˜ ์ƒํ’ˆ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” API ํ˜ธ์ถœ์€ ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ž‘๋™ํ•˜๊ณ  promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— getBy๋กœ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†๋‹ค. ๋Œ€์‹ ์— findBy... ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ •ํ•ด์ง„ ์‹œ๊ฐ„๋™์•ˆ ์—ฌ๋Ÿฌ๋ฒˆ ์žฌ์‹œ๋„๋ฅผ ๋ฐ˜๋ณตํ•ด api ์‘๋‹ต์ด ์™„๋ฃŒ๋˜์–ด ์™„์ „ํžˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

it('๋กœ๋”ฉ์ด ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ ์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ๋ชจ๋‘ ๋…ธ์ถœ๋œ๋‹ค', async () => {
  await render(<ProductList limit={PRODUCT_PAGE_LIMIT} />);

  const productCards = await screen.findAllByTestId('product-card');

  expect(productCards).toHaveLength(PRODUCT_PAGE_LIMIT);

ํ†ตํ•ฉํ…Œ์ŠคํŠธ์—์„œ์˜ ์ŠคํŒŒ์ด ํ•จ์ˆ˜

๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ŠคํŒŒ์ด ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ํ•จ์ˆ˜์˜ ํ˜ธ์ถœ ์—ฌ๋ถ€์™€ ์ „๋‹ฌ๋˜๋Š” ์ธ์ž๋“ค์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—์„œ๋„ ์ŠคํŒŒ์ด ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ ์ˆ˜์—…์—์„œ ์‚ฌ์šฉํ•œ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

it('์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฒ„ํŠผ ํด๋ฆญ์‹œ "์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ!" toast๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ, addCartItem ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.', async () => {
    const addCartItemFn = vi.fn();
    mockUseCartStore({ addCartItem: addCartItemFn });

    const { user } = await render(<ProductList limit={PRODUCT_PAGE_LIMIT} />);

    await screen.findAllByTestId('product-card');

    // ์ฒซ๋ฒˆ์งธ ์ƒํ’ˆ์„ ๋Œ€์ƒ์œผ๋กœ ๊ฒ€์ฆํ•œ๋‹ค.
    const productIndex = 0;
    const product = data.products[productIndex];
    await user.click(
      screen.getAllByRole('button', { name: '์žฅ๋ฐ”๊ตฌ๋‹ˆ' })[productIndex],
    );

    expect(addCartItemFn).toHaveBeenNthCalledWith(1, product, 10, 1);
    expect(
      screen.getByText(`${product.title} ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ!`),
    ).toBeInTheDocument();
  });
});

addCartItem์ด๋ผ๋Š” ์Šคํ† ์–ด ์•ก์…˜์„ ์ŠคํŒŒ์ดํ•จ์ˆ˜๋กœ ๋Œ€์ฒดํ•˜๊ณ  ์žˆ๋‹ค. src/utils/test/mockZustandStore.jsx ์— ์ฃผ์Šคํƒ ๋“œ ๋ชจํ‚น ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•ด ๋‘์—ˆ๋Š”๋ฐ

// ProductList.jsx
const { addCartItem } = useCartStore(state => pick(state, 'addCartItem'));
...

const handleClickCart = (ev, product) => {
	ev.stopPropagation();
	if (isLogin) {
	  addCartItem(product, user.id, 1);
	  toast.success(`${product.title} ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ์™„๋ฃŒ!`, { id: TOAST_ID });
	} else {
	  navigate(pageRoutes.login);
	}
};

ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ ๋ Œ๋”๋ง ๋œ ์ปดํฌ๋„ŒํŠธ๋Š” ์ € handleClickCart๊ฐ€ ์‹คํ–‰๋  ๋•Œ ์ฃผ์Šคํƒ ๋“œ์— ์ •์˜๋˜์–ด ์žˆ๋Š” ๊ธฐ์กด์˜ addCartItem์ด ์•„๋‹Œ ์ŠคํŒŒ์ด ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.