🕌

How to test React with Jest (日本語訳)

2023/11/20に公開

以下の記事を日本語訳したものです。

https://www.robinwieruch.de/react-testing-jest/


Jest はJavaScript、特にReactアプリケーションをテストするためにFacebookによって導入されました。現在、Reactコンポーネントをテストする最も一般的な方法の1つとなっています。独自のテストランナーがついているので、コマンドラインからJestを呼び出すだけで、すべてのテストを実行できます。すべてのテストはテストスイート(describe-blockなど)とテストケース(it-blockやtest-blockなど)として定義されています。

Jestのセットアップでは、オプションの設定を追加したり、セットアップルーチンを導入したり、テストを実行するためのカスタムやnpmスクリプトを定義したりすることができます。この記事のチュートリアルでは、そのすべてを実行する方法を学ぶことができます。すべての設定をすることは難しいため、Jestにはテストのアサーション (たとえば true を true に等しくする) のための豊富な APIが用意されています。このチュートリアルでは、ReactコンポーネントやJavaScript関数でこれらのテストアサーションを使用する方法を紹介します。また、Reactコンポーネントをテストするためのスナップショットテストについても紹介します。


reactセットアップでのjestテスト

テストのセットアップを実装し、最初のReactコンポーネントのテストを書く前に、最初にテストできる簡単なReactアプリケーションが必要です。src/index.jsファイルをインポートして、まだ実装されていないAppコンポーネントをレンダリングするところから始めてみましょう:

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(<App />, document.getElementById('app'));

src/App.jsファイルのAppコンポーネントは、React Hooksを持つReact Function Component になります。サードパーティライブラリとしてaxiosを使用するので、コマンドラインでnpm install axiosを使ってReactアプリケーションのnodeパッケージをインストールしてください。

import React from 'react';
import axios from 'axios';

export const dataReducer = (state, action) => {
  if (action.type === 'SET_ERROR') {
    return { ...state, list: [], error: true };
  }

  if (action.type === 'SET_LIST') {
    return { ...state, list: action.list, error: null };
  }

  throw new Error();
};

const initialData = {
  list: [],
  error: null,
};

const App = () => {
  const [counter, setCounter] = React.useState(0);
  const [data, dispatch] = React.useReducer(dataReducer, initialData);

  React.useEffect(() => {
    axios
      .get('http://hn.algolia.com/api/v1/search?query=react')
      .then(response => {
        dispatch({ type: 'SET_LIST', list: response.data.hits });
      })
      .catch(() => {
        dispatch({ type: 'SET_ERROR' });
      });
  }, []);

  return (
    <div>
      <h1>My Counter</h1>
      <Counter counter={counter} />

      <button type="button" onClick={() => setCounter(counter + 1)}>
        Increment
      </button>

      <button type="button" onClick={() => setCounter(counter - 1)}>
        Decrement
      </button>

      <h2>My Async Data</h2>

      {data.error && <div className="error">Error</div>}

      <ul>
        {data.list.map(item => (
          <li key={item.objectID}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
};

export const Counter = ({ counter }) => (
  <div>
    <p>{counter}</p>
  </div>
);

export default App;

Reactアプリケーションは2つのことを行っています:

  • まず、カウンタ・プロパティをレンダリングするためのpropsを受け取る Counterコンポーネントをレンダリングする。カウンタ・プロパティは、useState React HookでAppコンポーネント内のステートとして管理されていて、さらに、カウンタのステートは、ステートをインクリメントとデクリメントすることで、2つのボタンで更新できます。

  • 2つ目は、Appコンポーネントが初めてレンダリングされるときに、サードパーティのAPI からデータをフェッチ することです。ここではReact's useReducer Hookを使ってデータの状態を管理しています。エラーの場合は、エラーメッセージをレンダリングします。データがある場合は、Reactコンポーネントにアイテムのリストとしてデータをレンダリングします。

2つのコンポーネントとreducer関数は、後でテストファイルでテストできるように、すでにファイルからエクスポートしていることに注意してください。こうすることで、すべてのコンポーネントとレデューサを分離してテストすることができます。特にレデューサ関数は、ある状態から別の状態への状態遷移をテストするのに適しています。これが本当のユニットテストと呼ばれるものだ: 関数は入力でテストされ、テストは期待される出力をアサートします。

さらに、2つのReactコンポーネントは親と子の関係にあり、統合テストとしてテストできるシナリオとなります。それぞれのコンポーネントを単独でテストするのであれば、ユニットテストが必要です。しかし、例えば親コンポーネントと子コンポーネントをレンダリングするなど、それぞれのコンテキストで一緒にテストすることで、両方のコンポーネントの統合テストができ流ようになります。

テストを立ち上げて実行するために、Jestを開発依存としてコマンドラインにインストールしてセットアップします:

npm install --save-dev jest

package.json ファイルに、Jestを実行するためのnpmスクリプトが作成される。

{
  ...
  "scripts": {
    "start": "webpack serve --config ./webpack.config.js --mode development",
    "test": "jest"
  },
  ...
}

さらに、Jest を使って書くテストでは、もっといろいろな設定をしたいものです。そこで、追加の Jest 設定ファイルを Jest スクリプトに渡します:

{
  ...
  "scripts": {
    "start": "webpack serve --config ./webpack.config.js --mode development",
    "test": "jest --config ./jest.config.json"
  },
  ...
}

次に、コンフィギュレーション・ファイルでJestのオプション・コンフィギュレーションを定義します。コマンドラインで作成します:

touch jest.config.json

この Jest 設定ファイルに、Jest が実行するすべてのテストファイルを実行するために、以下のテストパターンマッチングを追加します:

{
  "testRegex": "((\\.|/*.)(spec))\\.js?$"
}

testRegex 設定は正規表現で、 Jest のテストが置かれるファイルの名前を指定します。この場合、ファイルは *spec.js という名前になります。こうすることで、src/ フォルダ内の他のファイルと明確に分けることができます。最後に、新しいsrc/App.spec.jsファイルに、Appコンポーネントのファイルの隣にテストファイルを追加します。まず、コマンドラインでテスト・ファイルを作成してみましょう:

touch src/App.spec.js

そして第二に、テスト・スイートの最初のテストケースを、この新しいファイルに実装をします:

describe('My Test Suite', () => {
  it('My Test Case', () => {
    expect(true).toEqual(true);
  });
});

これで npm test を実行して、テストケースを含むテストスイートを実行できるはずだ。先ほどのテストケースでは緑色(有効、成功)になっているはずですが、テストを別のもの、例えば expect(true).toEqual(false); に変更すると、赤色(無効、失敗)します。
これでJestを使った最初のテストが実行できました!

最後に、Jestテストを監視するためのnpmスクリプトをもうひとつ追加しましょう。このコマンドを使うことで、あるコマンドラインタブでテストを継続的に実行させながら、別のコマンドラインタブでアプリケーションを起動させることができます。アプリケーションの開発中にソースコードを変更するたびに、この監視スクリプトでテストが再度実行されます。

{
  ...
  "scripts": {
    "start": "webpack serve --config ./webpack.config.js --mode development",
    "test": "jest --config ./jest.config.json",
    "test:watch": "npm run test -- --watch"
  },
  ...
}

これで、ウォッチモードでJestテストを実行できるようになります。このようにすると、npm run test:watchでウォッチモードのJestテストを実行するためのターミナル・タブと、npm startでReactアプリケーションを起動するためのターミナル・タブを1つずつ開くことになる。ソースファイルを変更するたびに、ウォッチモードのおかげでテストが再度実行されるはずです。


reactにおけるJestのスナップショットテスト

Jestはいわゆるスナップショットテストを導入しました。基本的に、スナップショットテストは、テストを実行したときにレンダリングされたコンポーネントの出力のスナップショット(別のファイルに保存されます)を作成します。このスナップショットは、テストを再度実行するときに、次のスナップショットとの差分をとるために使われます。レンダリングコンポーネントの出力が変更された場合、両方のスナップショットの差分にはそれが表示され、スナップショットテストは失敗します。スナップショットテストは、レンダリングコンポーネントの出力が変更された場合にのみ通知されるはずなので、これは全く悪いことではありません。スナップショットテストが失敗した場合、変更を受け入れるか、変更を拒否してレンダリング出力に関するコンポーネントの実装を修正することができます。

スナップショットテストに Jest を使用することで、コンポーネントの実装の詳細を気にすることなく、テストを軽量に保つことができます。Reactでこれらがどのように機能するか見てみよう。まず、テストで実際のコンポーネントをレンダリングするために、Jestでよく使われるユーティリティ・ライブラリのreact-test-rendererをインストールします:

npm install --save-dev react-test-renderer

次に、Jestで最初のスナップショットテストを実装します。まず、新しいレンダラーでコンポーネントをレンダリングし、それをJSONに変換して、スナップショットを以前に保存したスナップショットと照合します:

import React from 'react';
import renderer from 'react-test-renderer';

import { Counter } from './App';

describe('Counter', () => {
  test('snapshot renders', () => {
    const component = renderer.create(<Counter counter={1} />);
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

これで、Jest のテストを再びウォッチモードで実行できるようになります。Snapshot Tests を配置した状態でテストをウォッチモードで実行すると、Jest を使ってテストをインタラクティブに実行できるようになります。たとえば、ウォッチモードがアクティブになったら、Reactコンポーネントのdiv要素をspan要素に変更します:

export const Counter = ({ counter }) => (
  <span>
    <p>{counter}</p>
  </span>
);

ウォッチモードでテストを実行しているコマンドラインには、失敗したスナップショットテストが表示されているはずです:

Counter
    ✕ snapshot renders (21ms)

  ● Counter › snapshot renders

    expect(received).toMatchSnapshot()

    Snapshot name: `Counter snapshot renders 1`

    - Snapshot
    + Received

    - <div>
    + <span>
        <p>
          1
        </p>
    - </div>
    + </span>

Watch Usage: Press w to show more.

以前のスナップショットは、Reactコンポーネントの新しいスナップショットと一致しなくなりました。さらに、コマンドラインは今すぐできることを提供してくれる(オプションで、キーボードのwを押す必要がある):

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

a または f を押すと、失敗したテストをすべて、または失敗したテストだけを実行します。uを押すと、「失敗した」テストを有効なものとして受け入れ、React コンポーネントの新しいスナップショットが保存されます。新しいスナップショットとして受け入れたくない場合は、コンポーネントを修正することでテストを修正してください。

export const Counter = ({ counter }) => (
 <div>
   <p>{counter}</p>
 </div>
);

その後、スナップショットテストが再び緑色に変わるはずです:

 PASS  src/App.spec.js
  Counter
    ✓ snapshot renders (17ms)

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

Watch Usage: Press w to show more.

とにかく、コンポーネントを変更し、新しいスナップショットを受け入れるか、Reactコンポーネントを再度修正するかして、自分で試してみてください。また、Appコンポーネントのスナップショットテストも追加してください:

import React from 'react';
import renderer from 'react-test-renderer';

import App, { Counter } from './App';

describe('App', () => {
  test('snapshot renders', () => {
    const component = renderer.create(<App />);
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

describe('Counter', () => {
  test('snapshot renders', () => {
    const component = renderer.create(<Counter counter={1} />);
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
  });
});

ほとんどの場合、スナップショットテストはどのReactコンポーネントでも同じように見えます。コンポーネントをレンダリングし、そのレンダリング出力をJSONに変換して比較できるようにし、前回のスナップショットと照合します。Snapshot Testsがあることで、Reactコンポーネントのテストはより軽量になります。また、Snapshot Testsは、実装ロジックを明示的にテストしないので、ユニットテストや統合テストを補完するために完璧に使用できます。

注: CSS-in-JSのためにReactでStyled Components を使用している場合は、スナップショットテストを使ってCSSスタイル定義をテストするためにjest-styled-components をチェックしてください。


react における Jest ユニットテスト/統合テスト

Jestは、JavaScriptのロジックを統合テストや単体テストとしてテストするのにも使えます。例えば、アプリコンポーネントがReact Hookを使ってデータを取得し、その結果をreducer関数でステートとして保存するとします。このレデューサ関数は、Reactについて何も知らないスタンドアロンのJavaScript関数としてエクスポートされます。したがって、Reactコンポーネントのレンダリングは必要なく、このレデューサー関数を普通のJavaScript関数としてテストできます。

import React from 'react';
import renderer from 'react-test-renderer';

import App, { Counter, dataReducer } from './App';

const list = ['a', 'b', 'c'];

describe('App', () => {
  describe('Reducer', () => {
    it('should set a list', () => {
      const state = { list: [], error: null };
      const newState = dataReducer(state, {
        type: 'SET_LIST',
        list,
      });

      expect(newState).toEqual({ list, error: null });
    });
  });

  ...
});

reducer関数の他の部分とエッジケースをカバーするために、さらに2つのテストを書きましょう。これらの2つの部分は、成功した結果を想定していないので、 "not so happy" - path と呼ばれます (たとえば、データのフェッチが失敗した場合など)。このようにテストを書くことで、アプリケーションのロジックにおけるすべての条件分岐をカバーすることができます。

import React from 'react';
import renderer from 'react-test-renderer';

import App, { Counter, dataReducer } from './App';

const list = ['a', 'b', 'c'];

describe('App', () => {
  describe('Reducer', () => {
    it('should set a list', () => {
      const state = { list: [], error: null };
      const newState = dataReducer(state, {
        type: 'SET_LIST',
        list,
      });

      expect(newState).toEqual({ list, error: null });
    });

    it('should reset the error if list is set', () => {
      const state = { list: [], error: true };
      const newState = dataReducer(state, {
        type: 'SET_LIST',
        list,
      });

      expect(newState).toEqual({ list, error: null });
    });

    it('should set the error', () => {
      const state = { list: [], error: null };
      const newState = dataReducer(state, {
        type: 'SET_ERROR',
      });

      expect(newState.error).toBeTruthy();
    });
  });

  ...
});

テストを実行すると、コマンドラインに以下のような出力が表示されるはずです。もしテストが失敗した場合、例えばウォッチ・モード中に失敗した場合、即座に通知されます。

You should get a similar output:

 PASS  src/App.spec.js
  App
    ✓ snapshot renders (18ms)
    Reducer
      ✓ should set a list (7ms)
      ✓ should reset the error if list is set (1ms)
      ✓ should set the error
  Counter
    ✓ snapshot renders (19ms)

Test Suites: 1 passed, 1 total
Tests:       5 passed, 5 total
Snapshots:   2 passed, 2 total
Time:        2.325s
Ran all test suites.

Watch Usage: Press w to show more.

Jestが普通のJavaScript関数のテストにも使えることはお分かりいただけただろう。Reactだけに使う必要はありません。もしあなたのアプリケーションにもっと複雑な関数があるなら、ためらわずにそれらをスタンドアロン関数として取り出して、テスト可能にエクスポートしてください。そうすれば、複雑なビジネスロジックがJestアサーションによってカバーされているため、常に動作することが保証されます。


Jestは、Reactコンポーネントのテストに必要な(ほぼ)すべてを提供します。コマンドラインからすべてのテストを実行したり、追加の設定を与えたり、テストスイートやテストケースをテストファイルに定義したりできます。スナップショットテストは、レンダリングされた出力を以前の出力と差分するだけで、Reactコンポーネントをテストする軽量な方法を提供します。また、JestがJavaScriptの関数だけをテストするために使用できることはお分かりいただけたと思います。

しかし、Jestを使ってReactコンポーネントのDOMをテストするのはより困難です。そのため、Reactコンポーネントのユニットテストを可能にするReact Testing LibraryやEnzymeのようなサードパーティのライブラリが存在します。Reactのテスト例については、チュートリアルシリーズを参照してください。

このチュートリアルは全3回のうちの第2回です。

第1回:【WebpackとBabelでReactをセットアップする方法】(https://www.robinwieruch.de/minimal-react-webpack-babel-setup/)
第3回: JestとEnzymeでReactコンポーネントをテストする方法
続きを読む: Jest Snapshot Testsを浅くレンダリングする方法
続きを読む: Jestスナップショットテストの違い
続きを読む: React-Redux接続コンポーネントのテスト方法

Discussion