どうやってReact Function ComponentをMockするか

## 検証環境

Jest

React Testing Library

Create React App(上記が含まれるので)


## 背景

サードパーティ製のComponentを避けてテストを行うために、ComponentをMockする必要がありました。

特に、Amplify-uiなど事前設定が必要なComponentに対しては有効な手段となります。


## テスト環境を作成

```bash
# 作業フォルダを作成
mkdir work && cd work

# Package ManagerでCRAを利用してReact Applicationを生成する
pnpm create react-app jest-app --template typescript

# フォルダに移動
cd jest-app

# componentsとtestフォルダを作成
mkdir src/components src/tests

# Editorを開く
code .
```

Componentを作成

以下をサードパーティ製のComponentやテスト範囲外の"複雑なComponent"と仮定します。

```tsx:src/components/complex-component.tsx
export default function ComplexDefaultComponent() {
  return <div>Default exported complex component</div>;
}

export function ComplexNamedFunction() {
  return <div>Named exported complex component</div>;
}
```

以下がテスト対象のComponentです。上記の"複雑なComponent"を用いて構成されています。

```tsx:src/components/parent-component.tsx
import ComplexDefaultComponent, { ComplexNamedFunction } from './complex-component';

export default function ParentComponent() {
  return (
    <div>
      <ComplexDefaultComponent />
      <ComplexNamedFunction />
      <div>Parent component</div>
    </div>
  );
}

```

**Mock無し**のテストをまず生成します。

```tsx:src/tests/parent-component.test.tsx
import { render, screen } from '@testing-library/react';
import ParentComponent from '../components/parent-component';

test('ParentComponent', () => {
  render(<ParentComponent />);

  screen.debug();

  // Mock無し場合は、当然複雑なComponentがそのまま表示されます。
  expect(screen.getByText('Default exported complex component')).toBeInTheDocument();
  expect(screen.getByText('Named exported complex component')).toBeInTheDocument();
});

```

```html:screen.debug()
    <body>
      <div>
        <div>
          <div>
            Default exported complex component
          </div>
          <div>
            Named exported complex component
          </div>
          <div>
            Parent component
          </div>
        </div>
      </div>
    </body>
```


## 実際にMockしてみる

`jest.mock()`を使用します。
※`import * as xxx from 'xxx'`とすれば、jest.sypOn()でも可能ですが、手間が増えるので採用しません。

Default exportとNamed exportのMockのされ方に注意してください。
以下のようにテストファイルを書き換えます。Mockされた内容でテストは成功します。

```tsx:src/tests/parent-component.test.tsx

import { render, screen } from '@testing-library/react';
import ParentComponent from '../components/parent-component';

// Jest内でHoistingされるので、jest.mockはModuleスコープで宣言する必要があります
jest.mock('../components/complex-component', () => {
  return {
    // Default exportされるObjectを明示的にexportするためのｄフラグ
    __esModule: true,
    // Default export
    default: () => <div>Mock default export</div>,
    // Named export
    ComplexNamedFunction: () => <div>Mock named export</div>,
  };
});

test('ParentComponent', () => {
  render(<ParentComponent />);

  screen.debug();

  // 本来のComponentは表示されません
  expect(screen.queryByText('Default exported complex component')).not.toBeInTheDocument();
  expect(screen.queryByText('Named exported complex component')).not.toBeInTheDocument();

  // MockされたComponentが表示されます
  expect(screen.getByText('Mock default export')).toBeInTheDocument();
  expect(screen.getByText('Mock named export')).toBeInTheDocument();
});

```

もちろん中身もMockされていますね。

```html:screen.debug()
    <body>
      <div>
        <div>
          <div>
            Mock default export
          </div>
          <div>
            Mock named export
          </div>
          <div>
            Parent component
          </div>
        </div>
      </div>
    </body>
```


## 最後に

上記のようにMockすることで、テスト範囲を限定し、効果的なテストを行うことが可能です。

ちなみに記事によっては、`jest.mock('xxx', () => () => {return <div>ooo</div>})`のように、`default`や`Named export`を明示せずともMockできるとありましたが、私の環境ではエラーが発生し、うまくいきませんでした。

テストコードは、コードレビューやモジュール分離などのヒントも与えてくれます。ドキュメント上のテストの仕組みだけでなく、実際に手を動かすことで見えてくるものがあるので、これからも自分に負けずにテストを書いていきたいです。


## 参考

[公式のjest.mock()](https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options)

[この方法は可能？](https://timmousk.com/blog/jest-mock-component/)