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-hooks`の`renderHook`を呼び出して結果を確認すればOKです。`renderHook`は`result.current`配下に実際に呼び出したcustom hooksの結果を入れてくれるので、この場合`result.current.awesomeFuncC()`を呼び出すことでuseAwesomeCのロジックをテストします。

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

  return {awesomeFuncC};
};

export default useAwesomeC;
```
```typescript: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`の使い方を理解すれば簡単です。インストール方法やその他の情報は[ライブラリのサイト](https://react-hooks-testing-library.com/)で確認できます。

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

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


```typescript: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;
```
```typescript: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でモック方法に違いがあるかなと思って実験しましたが、何も変わりませんでした。

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