jestを使ったCustom hooksのunit test方法 (別のCustom hooksに依存している場合)


Custom Hooksを利用していてそれらの間に依存関係があるとき、どうやってjestのテストを書くか、です。

結論から言うと、jestのモック化の方法を覚えましょうという話でした。

前提

useAwesomeA, B, C, Dがあり、useAwesomeAからB, C, Dへの依存がある状態で、B, Dにunit testでは実行したくないロジック(サーバ通信やReact nativeだとnative moduleの処理など)があるという状態を前提としています。

useAwesomeA --+--> useAwesomeB (unit testで呼び出したく無い)
              +--> useAwesomeC
              +--> useAwesomeD (unit testで呼び出したく無い)

言語はTypescriptです。

他のCustom Hooksに依存関係が無い場合

@testing-library/react-hooksrenderHookを呼び出して結果を確認すればOKです。renderHookresult.current配下に実際に呼び出したcustom hooksの結果を入れてくれるので、この場合result.current.awesomeFuncC()を呼び出すことでuseAwesomeCのロジックをテストします。

useAwesomeC.ts
const useAwesomeC = () => {
  const awesomeFuncC: () => string = () => {
    return 'message from C!';
  };

  return {awesomeFuncC};
};

export default useAwesomeC;
useAwesomeC.test.ts
import {renderHook} from '@testing-library/react-hooks';
import useAwesomeC from '../../src/hooks/useAwesomeC';

describe('Custom Hookのテスト方法', () => {
  test('他のCustom Hookに依存していないCustom Hookのテスト', () => {
    const {result} = renderHook(() => useAwesomeC());

    expect(result.current.awesomeFuncC()).toBe('message from C!');
  });
});

renderHookの使い方を理解すれば簡単です。インストール方法やその他の情報はライブラリのサイトで確認できます。

他のCustom Hooksに依存がある場合

jestで呼び出したくないモジュールをモック化します。モック化の方法はいくつかあるようですが今回はtestコードの中でモック化しています。

手順は以下の通りです。
1. テストコードでモックしたいモジュールをimportする
2. jest.mockでモジュールをモック化する
3. mockImplementation()でモック化したモジュールが何を返すか実装する
4. mockClear()でテスト完了時に呼び出された回数などの記録をクリア

useAwesomeA.ts
import {useCallback} from 'react';
import useAwesomeB from './useAwesomeB';
import useAwesomeC from './useAwesomeC';
import {useAwesomeD} from './useAwesomeD';

const useAwesomeA = () => {
  const {awesomeFuncB} = useAwesomeB();  // unit testでは呼び出したくない
  const {awesomeFuncC} = useAwesomeC();
  const {awesomeFuncD} = useAwesomeD();  // unit testでは呼び出したくない

  const awesome = useCallback(() => {
    const messageB = awesomeFuncB();
    const messageC = awesomeFuncC();
    const messageD = awesomeFuncD();
    return `Hellow! Here are messages for you!: ${messageB}, ${messageC}, ${messageD}`;
  }, [awesomeFuncB, awesomeFuncC, awesomeFuncD]);

  return {awesome};
};

export default useAwesomeA;
useAwesomeA.test.ts
import {renderHook} from '@testing-library/react-hooks';
import useAwesomeA from '../../src/hooks/useAwesomeA';
import useAwesomeB from '../../src/hooks/useAwesomeB';   // 一度普通にimportする(default export)
import {useAwesomeD} from '../../src/hooks/useAwesomeD'; // 一度普通にimportする(名前付きexport)

jest.mock('../../src/hooks/useAwesomeB'); // moduleをmockする
jest.mock('../../src/hooks/useAwesomeD'); // moduleをmockする

describe('Custom Hookのテスト方法', () => {
  const mockMessageB = 'message from mockB';
  const mockMessageD = 'message from mockD';

  beforeAll(() => {
    // mock moduleで何を返すかを設定する
    (useAwesomeB as jest.Mock).mockImplementation(() => ({
      awesomeFuncB: jest.fn().mockReturnValue(mockMessageB),
    }));

    (useAwesomeD as jest.Mock).mockImplementation(() => ({
      awesomeFuncD: jest.fn().mockReturnValue(mockMessageD),
    }));
  });

  beforeEach(() => {
    // 呼び出し回数などをクリア
    (useAwesomeB as jest.Mock).mockClear();
    (useAwesomeD as jest.Mock).mockClear();
  });

  test('Custom Hookが別のCustom Hookを呼び出しているときののテスト', () => {
    const {result} = renderHook(() => useAwesomeA());

    expect(result.current.awesome()).toBe(
      `Hellow! Here are messages for you!: ${mockMessageB}, message from C!, ${mockMessageD}`,
    );
  });
});

ふと、default exportと名前付きexportでモック方法に違いがあるかなと思って実験しましたが、何も変わりませんでした。

以上、誰かの役に立てば幸いです。