๊ฐœ์ธ๊ณต๋ถ€/TDD

React Testing: Hooks testing

soon327 2021. 11. 21. 23:03


๐Ÿง Hooks testing?

๋ฆฌ์•กํŠธ ํ”„๋กœ์ ํŠธ์—์„œ react-hooks๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, ๊ณตํ†ต๋œ ๋กœ์ง์ด๋“  ์ƒํƒœ๊ด€๋ จ ๋กœ์ง์ด๋“  ๋‹ค์–‘ํ•œ custom hook์„ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๊ฒŒ๋œ๋‹ค.
๋”ฐ๋ผ์„œ reducer๋‚˜ component ํ…Œ์ŠคํŒ…๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ด๋Ÿฌํ•œ custom hook๋“ค์ด ์˜๋„๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€๋„ ํ…Œ์ŠคํŒ…ํ•ด์ฃผ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.
ํ•˜๋‚˜์˜ component์—์„œ ์‚ฌ์šฉ๋˜๋Š” hook์ด๊ฑฐ๋‚˜ ๊ฐ„๋‹จํ•œ ๋กœ์ง์˜ hook์ด๋ผ๋ฉด ์ง์ ‘ ํ…Œ์ŠคํŒ…ํ•˜๋Š” ๊ฒƒ์ด ์‰ฝ๊ฒ ์ง€๋งŒ,
๋ณต์žกํ•œ ๋กœ์ง์˜ hook์ด๋‚˜ API์š”์ฒญ์ด ์žˆ๋Š” hook๋“ฑ์€ ์ง์ ‘ ํ…Œ์ŠคํŒ…ํ•˜๋Š” ๊ฒƒ์ด ์กฐ๊ธˆ์€ ๋ณต์žกํ•  ์ˆ˜ ์žˆ๋‹ค.
testing-library/react-hook ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ hook๋“ค์„ ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŒ…ํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ“š testing-library/react-hooks

testing-library/react-hooks์˜ API ์ค‘ ๊ฐ€์žฅ ํ”ํ•˜๊ฒŒ ์“ฐ์ด๋Š” ๊ฒƒ๋“ค์€ renderHook, act์ด๋‹ค.
renderHook์€ ์ปค์Šคํ…€ํ›…์„ ๋”ฐ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“คํ•„์š”์—†์ด ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒํ•ด์ฃผ๊ณ 
act๋Š” ์ปค์Šคํ…€ํ›…์„ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ์“ฐ์ด๋Š” API์ด๋‹ค.
๊ทธ์™ธ ๋‹ค์–‘ํ•œ API๋“ค์€ ์ด๊ณณ์„ ์ฐธ๊ณ ํ•˜๋ฉด ๋œ๋‹ค.

์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ์ฝ”๋“œ

// useFilter.ts
export function useFilter() {
  const filter = useRootState((state) => state.filter);
  const dispatch = useDispatch();
  const actions = useMemo(() => bindActionCreators(filterActions, dispatch), [dispatch]);
  return [filter, actions.applyFilter] as const;
}
// useFilter.test.ts

import { act, renderHook } from '@testing-library/react-hooks';
import prepareMockReduxWrapper from '@/lib/prepareMockReduxWrapper';
import { filterActions } from '@/_reducer/filter';
import { useFilter } from '@/hooks/useFilter';
import configureMockStore from 'redux-mock-store';

describe('useFilter', () => {
  const setup = () => {
    const [wrapper, store] = prepareMockReduxWrapper({
      filter: 'ALL',
      todos: [],
    });
    const { result } = renderHook(() => useFilter(), { wrapper });
    return { store, result };
  };

  ...

  it('confirm dispatch', () => {
    const { store, result } = setup();
    // applyFilter ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ 
    act(() => {
      result.current[1]('DONE');
    });
    // ํ•ด๋‹น ์•ก์…˜์ด ๋””์ŠคํŒจ์น˜ ๋๋Š”์ง€ ํ™•์ธ
    expect(store.getActions()).toEqual([filterActions.applyFilter('DONE')]);
  });

    ...
});

์œ„์˜ prepareMockReduxWrapper์€ redux-mock-store๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ initial state๋ฅผ ๋ฐ›๊ณ , wrapper์™€ store์„ ๋ฆฌํ„ดํ•˜๋Š” ํ•จ์ˆ˜๋‹ค.
renderHook์„ ์‚ฌ์šฉํ•˜๋ฉด ์œ„์ฒ˜๋Ÿผ ํŠน์ • wrapper๋กœ ๊ฐ์‹ธ์ค„ ์ˆ˜๋„ ์žˆ๊ณ , ์ด๊ฒƒ์™ธ์—๋„ ๋‹ค์–‘ํ•œ option์„ ์ถ”๊ฐ€ํ•˜์—ฌ ์ปค์Šคํ…€ํ›…์„ ํ…Œ์ŠคํŒ…ํ•  ์ˆ˜ ์žˆ๋‹ค.
renderHook์˜ result๊ฐ์ฒด์˜ current์—๋Š” ์ปค์Šคํ…€ํ›…์˜ ๋ฆฌํ„ด๊ฐ’์ด ํฌํ•จ๋˜์–ด์žˆ๋‹ค.
๋”ฐ๋ผ์„œ useFilter.ts์˜ ๋ฆฌํ„ด๊ฐ’ ์ค‘ filter์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” result.current[0]์œผ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ๋œ๋‹ค.
์ด๋Ÿฌํ•œ hook์„ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ์œ„ํ•ด์„œ actํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์˜€๊ณ , ์ œ๋Œ€๋กœ ์•ก์…˜์ด ๋””์ŠคํŒจ์น˜๋๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์˜€๋‹ค.

๋น„๋™๊ธฐ hooks ํ…Œ์ŠคํŒ…

testing-library/react-hooks๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” hooks ํ…Œ์ŠคํŒ…๋„ ๋„์™€์ค€๋‹ค.๐Ÿ‘

import { renderHook } from '@testing-library/react-hooks'
import { useCounter } from './counter'

test('should increment counter after delay', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useCounter())

  result.current.incrementAsync()

  await waitForNextUpdate()

  expect(result.current.count).toBe(1)
})

๋ฐ”๋กœ renderHook์˜ ๋ฆฌํ„ด๊ฐ’ ์ค‘ waitForNextUpdateํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.
await waitForNextUpdate()๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ปค์Šคํ…€ํ›…์˜ ๋น„๋™๊ธฐํ•จ์ˆ˜์ธ incrementAsyncํ•จ์ˆ˜๊ฐ€ ์™„๋ฃŒ๋ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ ๋’ค ํ…Œ์ŠคํŒ…ํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ™ ์ฐธ๊ณ