@testing-library/react-hookとJestでReact Hookをテストする

React #2 Advent Calendar 2020」4日目の記事です。

@testing-library/react-hookを使うと、React Hookのユニットテストがかけます。

Install

$ yarn add -D  react-@testing-library/react-hooks react-test-renderer

テスト対象のフックを作ってみる

動作確認のために、適当なフックを作ってみましょう。


const useExampleModalHook = () => {
    const [modal, modalOpen] = useState(false)
    const open = useCallback(() => {
      modalOpen(true)
    }, [modalOpen])
    const close = useCallback(() => {
      modalOpen(false)
    }, [modalOpen])
    const toggle = useCallback(() => {
      modalOpen(!modal)
    }, [modal, modalOpen])
    return {
      modal,
      open,
      close,
      toggle,
    }
  }

テストコードはこんな感じです。

import { renderHook, act } from '@testing-library/react-hooks';

describe('useExampleModalHook', () => {
  let result = renderHook(() => useExampleModalHook()).result
  beforeEach(() => {
    result = renderHook(() => useExampleModalHook()).result
  })
  afterEach(() => {
    mockDispatch.mockClear();
  });
  it('should modal is false by default', () => {
    expect(result.current.modal).toEqual(false)
  })
  it('should modal is true when call open', () => {
    act(() => {
      result.current.open()
    })
    expect(result.current.modal).toEqual(true)
  })
  it('should modal is true when called toggle once', () => {
    act(() => {
      result.current.toggle()
    })
    expect(result.current.modal).toEqual(true)
  })
  it('should modal is true when called toggle once', () => {
    act(() => {
      result.current.toggle()
    })
    act(() => {
      result.current.toggle()
    })
    expect(result.current.modal).toEqual(false)
  })
})

renderHook を使うことで、hook単体でテストコードが書けるようになります。

また actを使って関数を実行したり、unmountなどもテストしていけます。

With Redux (useDispatch / useSelector)

ただしReduxが絡むとモックが必要になります。

export const exampleHookWithRedux = () => {
  const dispatch = useDispatch();
  const globalClose = useCallback(() => {
    dispatch(closeUserModal('username'));
  }, [dispatch]);
  return {
    dummy: useSelector((state: State) => state.modal),
    globalClose,
  }
}

以下のようにJestでモックしてやりましょう。

const mockDispatch = jest.fn();
const mockSelector = jest.fn().mockImplementation(() => ({
  status: '',
  result: '',
}));
jest.mock('react-redux', () => ({
  useDispatch: () => mockDispatch,
  useSelector: () => mockSelector(),
}));


describe('exampleHookWithRedux ', () => {
  let result = renderHook(() => exampleHookWithRedux ()).result
  beforeEach(() => {
    result = renderHook(() => exampleHookWithRedux ()).result
  })
  it('should modal is false by default', () => {
    expect(result.current.dummy).toEqual({
      status: '',
      result: '',
    })
  })
  it('should call dispatch function at once', () => {
    act(() => {
      result.current.globalClose()
    })
    expect(mockDispatch).toHaveBeenCalledTimes(1)
  })
  it('should call valid action', () => {
    act(() => {
      result.current.globalClose()
    })
    expect(mockDispatch).toHaveBeenCalledWith({
      type: 'CLOSE_USER_MODAL',
      payload: {
        username: 'username'
      }
    })
  })
  it('When selector return updated value, should get updated one', () => {
    mockSelector.mockImplementationOnce(() => ({
      status: 'loading',
      result: ''
    }))
    result = renderHook(() => exampleHookWithRedux ()).result
    expect(result.current.dummy).toEqual({
      status: 'loading',
      result: '',
    })
  })
})

Comment