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์ด ์๋ ์คํ์ด ํจ์๋ฅผ ํธ์ถํ๋ค.
