ReactコンポーネントをJestでスナップショットテストする

# はじめに
最近Reactを勉強し始めた初心者です。今まで、バックエンドのユニットテストなどは書いたことがあるのですが、フロントエンドのテスト手法に、スナップショットテストというものがあると知り、作成してみました。また、今回は **create-react-app** を利用しているので、最初から使える **Jest** をテストフレームワークとして利用します。

# 目次
[1. 検証環境](#1-検証環境)
[2. スナップショットテストとは](#2-コンポーネントのスナップショットテストを作成する)
[3. コンポーネントのスナップショットテストを作成する](#3-コンポーネントのスナップショットテストを作成する)
[4. プログラムソース](#4-プログラムソース)
[5. 所感](#5-所感)

# 1. 検証環境
* OS: macOS Big Sur Version 11.1
* Chip: Apple M1
* typescript: 4.1.3
* react: 17.0.1
* jest: 26.6.3

# 2. スナップショットテストとは

スナップショットテストは、**予期しないUIの変更を確認したいとき**に、有用なテストです。一般的には、UIコンポーネントをレンダリングし、そのスナップショットを取得して、以前、取得した参照用のスナップショットと比較します。２つのスナップショットが一致しない場合、テストが失敗するため、何らかの変更が起こったときに簡単に検知することができます。

また、スナップショットの生成物は、コードの変更と一緒にコミットし、コードレビューのプロセスの一部としてレビューするします。Jestは、以下のように、コードレビューで人間が読めるように、可読性の高い形でスナップショットが生成されます。

```js
exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;
```

テストが失敗した場合（UIに変更が起こった場合）は、スナップショットの内容をレビューし、バグとして修正するか、実装された変更が正しいと判断し、スナップショットを更新する必要があります。

なんだか、今まで実装したことのある、ユニットテストとは少し感う印象です。

# 3. コンポーネントのスナップショットテストを作成する
## 3.1 フォルダ構成の決定
create-react-appを利用していれば、最初からJestは利用できるはずなので、フォルダ構成の決定から実施します。create-react-appでは、以下のパターンにマッチすると、テストコードとして認識します。

* `__tests__`フォルダ内にある、`.js/tsx`拡張子のファイル
* `.test.js/tsx`拡張子のファイル
* `.spec.js/tsx`拡張子のファイル

今回は、`scr/__tests__`を作成して`.test.tsx`拡張子でテストコードを作成したいと思います。フォルダ階層は以下です。

```
src
├── __tests__
│   └── components
│       └── molecules
│           └── Copyright.test.tsx
├── components
│   └── molecules
│       └── Copyright.tsx
```

## 3.2 テスト対象のコンポーネントの説明

フッターにコピーライトの文字を返すコンポーネントです。UIフレームワークには、**material-ui**を利用してます。

```Copyright.tsx
import { FC } from 'react';
import Typography from '@material-ui/core/Typography';

type CopyrightProps = {
  authorName: string;
};

const Copyright: FC<CopyrightProps> = ({ authorName }) => (
  <Typography variant="body2" color="textSecondary" align="center">
    {'© '}
    {new Date().getFullYear()} {authorName}.
  </Typography>
);

export default Copyright;

```

## 3.3 react-test-rendererのインストール
create-react-appを利用すると、jestは使えるのですが、`react-test-renderer`が入ってないので、インストールします。私はTypescriptを利用しているので、`@types/react-test-renderer` と `ts-jest` もインストールします。

```bash
yarn add --dev react-test-renderer
yarn add --dev @types/react-test-renderer
yarn add --dev ts-jest
```


## 3.4 テストコードの作成

以下のようにテストコードを作成しました。

```Copyright.test.tsx
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import Copyright from '../../../components/molecules/Copyright';

it('renders correctly', () => {
  const tree = renderer.create(<Copyright authorName="test1" />).toJSON();
  expect(tree).toMatchSnapshot();
});

```

## 3.5 テストの実行

以下のコマンドで、テストを実行してみましょう。

```bash
$ yarn test
 PASS  src/__tests__/components/molecules/Copyright.test.tsx
  Copyright componet
    ✓ renders correctly (14 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        0.787 s, estimated 1 s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.
```

無事テストが成功しました。テストを実行すると、テストファイルと同じ場所に、`__snapshopts__/Copyright.test.tsx.snap` というスナップショットが作成されました。内容は以下のようになっていました。

```Copyright.test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Copyright componet renders correctly 1`] = `
<p
  className="MuiTypography-root MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-alignCenter"
>
  © 
  2021
   
  test1
  .
</p>
`;

```

テストが正しく動くかを検証するために、テストコードのpropsを変更してみます。

```Copyright.test.tsx
import * as React from 'react';
import * as renderer from 'react-test-renderer';
import Copyright from '../../../components/molecules/Copyright';

describe('Copyright componet', () => {
  it('renders correctly', () => {
    const tree = renderer.create(<Copyright authorName="test2" />).toJSON();
    expect(tree).toMatchSnapshot();
  });
});

```

テストを再度実行すると、以下の様になりました。

```bash
$ yarn test
FAIL  src/__tests__/components/molecules/Copyright.test.tsx
  Copyright componet
    ✕ renders correctly (15 ms)

  ● Copyright componet › renders correctly

    expect(received).toMatchSnapshot()

    Snapshot name: `Copyright componet renders correctly 1`

    - Snapshot  - 1
    + Received  + 1

    @@ -2,8 +2,8 @@
        className="MuiTypography-root MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-alignCenter"
      >
        © 
        2021
         
    -   test1
    +   test2
        .
      </p>

       6 |   it('renders correctly', () => {
       7 |     const tree = renderer.create(<Copyright authorName="test2" />).toJSON();
    >  8 |     expect(tree).toMatchSnapshot();
         |                  ^
       9 |   });
      10 | });
      11 | 

      at Object.<anonymous> (src/__tests__/components/molecules/Copyright.test.tsx:8:18)

 › 1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        1.232 s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press u to update failing snapshots.
 › Press i to update failing snapshots interactively.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.
```
ちゃんと差分が検知され、テストが失敗しました。この変更が、意図したものであれば、`yarn test -u` でスナップショットを更新し、バグであれば修正するという流れになります。

## 3.6 可変値に対応する

これで、テストは一旦完了ですが、一つだけ修正したい場所があります。このコンポーネントですが、`new Date().getFullYear()` を利用しており、翌年になると一切変更を指定なくてもテストが失敗します。このような、コンポーネント内にIDや日付などの可変値を含む際の対処を実施します。

以下のように、mockを利用し、Dateが固定値を返すように変更します。

```Copyright.test.tsx
describe('<Copyright />', () => {
  it('should render correctly', () => {
    const mockDate = new Date(2021, 1, 1);
    const spy = jest
      .spyOn(global, 'Date')
      .mockImplementation(() => (mockDate as unknown) as string);

    const tree = renderer.create(<Copyright authorName="wkamuy" />).toJSON();
    expect(tree).toMatchSnapshot();

    spy.mockRestore();
  });
});

```

これで、テストは完了です。

# 4. プログラムソース

以下がプログラムソースです。

[WKAMUY's GitHub Repository](https://github.com/wkamuy/portfolio)

以下が実際のWEBページです

[WKAMUY.PAGES](https://wkamuy.github.io/portfolio/)

# 5. 所感
今回はスナップショットテストを作成しました。かなり手軽に作成できるので、UIのテストの一つとして導入するのは結構有用かなと思います。DOMの操作などもできるようなので、必要な部分は実装してみたいなと思います。