無気力エンジニア

大好きな技術の話や趣味の話を書きます

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

はじめに

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

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コンポーネントレンダリングし、そのスナップショットを取得して、以前、取得した参照用のスナップショットと比較します。2つのスナップショットが一致しない場合、テストが失敗するため、何らかの変更が起こったときに簡単に検知することができます。

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

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を利用してます。

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-rendererts-jest もインストールします。

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

3.4 テストコードの作成

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

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 テストの実行

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

$ 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 というスナップショットが作成されました。内容は以下のようになっていました。

// 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を変更してみます。

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();
  });
});

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

$ 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が固定値を返すように変更します。

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

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

WKAMUY.PAGES

5. 所感

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