Reactアプリをテスト
Facebook ではJestを使用して、Reactアプリケーションをテストします。
セットアップ
Create React Appを使用したセットアップ
Reactに馴染みがないなら、Create React Appの利用をお勧めします。 すぐに使えて Jestも同梱されています!  スナップショットをレンダリングするには、 react-test-renderer を追加するだけです。
実行
- npm
 - Yarn
 - pnpm
 
npm install --save-dev react-test-renderer
yarn add --dev react-test-renderer
pnpm add --save-dev react-test-renderer
Create React Appを使わないセットアップ
既存のアプリケーションがある場合は、いくつかのパッケージをインストールしてうまく機能するようにする必要があります。 babel-jestパッケージと reactのbabel presetをテスト環境内のコードを変換するのに利用しています。 using babelも参照して下さい。
実行
- npm
 - Yarn
 - pnpm
 
npm install --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
pnpm add --save-dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
package.jsonは以下のようなものになっているはずです( <current-version>はパッケージの実際の最新版のバージョンの数字になります)。 scriptsとJestの設定のエントリを追加して下さい:
{
  "dependencies": {
    "react": "<current-version>",
    "react-dom": "<current-version>"
  },
  "devDependencies": {
    "@babel/preset-env": "<current-version>",
    "@babel/preset-react": "<current-version>",
    "babel-jest": "<current-version>",
    "jest": "<current-version>",
    "react-test-renderer": "<current-version>"
  },
  "scripts": {
    "test": "jest"
  }
}
module.exports = {
  presets: [
    '@babel/preset-env',
    ['@babel/preset-react', {runtime: 'automatic'}],
  ],
};
それでは次へ進みましょう!
スナップショットテスト
ハイパーリンクをレンダリングするLinkコンポーネントの snapshot test を作成しましょう:
import {useState} from 'react';
const STATUS = {
  HOVERED: 'hovered',
  NORMAL: 'normal',
};
export default function Link({page, children}) {
  const [status, setStatus] = useState(STATUS.NORMAL);
  const onMouseEnter = () => {
    setStatus(STATUS.HOVERED);
  };
  const onMouseLeave = () => {
    setStatus(STATUS.NORMAL);
  };
  return (
    <a
      className={status}
      href={page || '#'}
      onMouseEnter={onMouseEnter}
      onMouseLeave={onMouseLeave}
    >
      {children}
    </a>
  );
}
例として関数コンポーネントを使用していますが、クラスコンポーネントも同じ方法でテストすることができます。 React: 関数コンポーネントとクラスコンポーネントを参照してください。 リマインド: クラスコンポーネントでは、メソッドを直接テストするのではなく、propsをテストするためにJestを使用することを想定しています。
コンポーネントとのやり取りとレンダリングされた出力をキャプチャしてスナップショットファイルを作成するために、ReactのテストレンダラーとJestのスナップショット機能を利用しましょう:
import renderer from 'react-test-renderer';
import Link from '../Link';
it('changes the class when hovered', () => {
  const component = renderer.create(
    <Link page="http://www.facebook.com">Facebook</Link>,
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();
  // manually trigger the callback
  renderer.act(() => {
    tree.props.onMouseEnter();
  });
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
  // manually trigger the callback
  renderer.act(() => {
    tree.props.onMouseLeave();
  });
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});
yarn test または jestを実行すると、このようなファイルが出力されます:
exports[`changes the class when hovered 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;
exports[`changes the class when hovered 2`] = `
<a
  className="hovered"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;
exports[`changes the class when hovered 3`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;
次回のテストでは、レンダリングされた出力は前に作成されたスナップショットと比較されます。 スナップショットは、コードの変更に沿ってコミットされるべきです。 スナップショットテストが失敗した場合、それが意図的な変更かどうかを点検する必要があります。 変更が予想されたものであればjest -uコマンドでJestを実行して既存のスナップショットを上書きします。
The code for this example is available at examples/snapshot.
モック、Enzyme、 React 16+ を使用したスナップショットテスト
Enzyme と React 16 以降を使用している場合、スナップショットテストには注意点があります。 以下のスタイルを使用しているモジュールをモックアウトする場合:
jest.mock('../SomeDirectory/SomeComponent', () => 'SomeComponent');
コンソールに次のような警告が表示されます。
Warning: <SomeComponent /> is using uppercase HTML. Always use lowercase HTML tags in React.
# または
Warning: The tag <SomeComponent> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
React 16 がこれらの警告を引き起こしてしまう理由は、要素の型チェックの方法のためであり、モック化したモジュールはこのチェックに引っかかってしまいます。 これに対処するための選択肢は以下のとおりです。
- テキストとしてレンダリングする。 この方法を選んだ場合、スナップショット内のモックコンポーネントに渡された props を確認することができませんが、シンプルで分かりやすい方法です。
js
jest.mock('./SomeComponent', () => () => 'SomeComponent'); - カスタム要素としてレンダリングする。 DOM "カスタム要素" は一切チェックされないため、警告も発生しません。 名前にダッシュと小文字が使われます。
tsx
jest.mock('./Widget', () => () => <mock-widget />); react-test-rendererを使用する。 test renderer は、要素の型を気にしないので、SomeComponentといった要素を許容してくれます。 test renderer を使うと、スナップショットをチェックしたり、Enzyme とは独立してコンポーネントのふるまいを確認することができます。- 警告をすべて無効にします(jestの設定ファイルで行う必要があります):警告をすべて無効にします(jestの設定ファイルで行う必要があります): js jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction')); これは、有用な警告が失われる可能性があるため、通常とるべき選択肢ではありません。 しかし、例えばreact-nativeのコンポーネントをテストする場合には、react-nativeタグをDOMにレンダリングする際に、多くの無関係な警告が発生します。 別の選択肢としては、console.warning を利用して、特定の警告を抑制することです。
jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction')); 
DOM のテスト
If you'd like to assert, and manipulate your rendered components you can use react-testing-library, Enzyme, or React's TestUtils. 以下の2つの例では、react-testing-library と Enzyme を使用します。
react-testing-library
- npm
 - Yarn
 - pnpm
 
npm install --save-dev @testing-library/react
yarn add --dev @testing-library/react
pnpm add --save-dev @testing-library/react
2つのラベルを入れ替えるチェックボックスを実装しましょう。
import {useState} from 'react';
export default function CheckboxWithLabel({labelOn, labelOff}) {
  const [isChecked, setIsChecked] = useState(false);
  const onChange = () => {
    setIsChecked(!isChecked);
  };
  return (
    <label>
      <input type="checkbox" checked={isChecked} onChange={onChange} />
      {isChecked ? labelOn : labelOff}
    </label>
  );
}
import {cleanup, fireEvent, render} from '@testing-library/react';
import CheckboxWithLabel from '../CheckboxWithLabel';
// Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher
// unmount and cleanup DOM after the test is finished.
afterEach(cleanup);
it('CheckboxWithLabel changes the text after click', () => {
  const {queryByLabelText, getByLabelText} = render(
    <CheckboxWithLabel labelOn="On" labelOff="Off" />,
  );
  expect(queryByLabelText(/off/i)).toBeTruthy();
  fireEvent.click(getByLabelText(/off/i));
  expect(queryByLabelText(/on/i)).toBeTruthy();
});
The code for this example is available at examples/react-testing-library.
Enzyme
- npm
 - Yarn
 - pnpm
 
npm install --save-dev enzyme
yarn add --dev enzyme
pnpm add --save-dev enzyme
If you are using a React version below 15.5.0, you will also need to install react-addons-test-utils.
react-testing-libraryの代わりにEnzymeを使用して上記のテストを書き直しましょう。 We use Enzyme's shallow renderer in this example.
import Enzyme, {shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import CheckboxWithLabel from '../CheckboxWithLabel';
Enzyme.configure({adapter: new Adapter()});
it('CheckboxWithLabel changes the text after click', () => {
  // Render a checkbox with label in the document
  const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
  expect(checkbox.text()).toBe('Off');
  checkbox.find('input').simulate('change');
  expect(checkbox.text()).toBe('On');
});
独自のコード変換処理
より高度な機能が必要な場合は、独自の変換処理を構築することもできます。 babel-jestを使用する代わりに、 @babel/core を使用する例を示します:
'use strict';
const {transform} = require('@babel/core');
const jestPreset = require('babel-preset-jest');
module.exports = {
  process(src, filename) {
    const result = transform(src, {
      filename,
      presets: [jestPreset],
    });
    return result || src;
  },
};
この例を動作させるには @babel/core とbabel-preset-jest パッケージをインストールすることを忘れないで下さい。
これをJestと動作させるにはJestに次の設定を追加する必要があります:"transform": {"\\.js$": "path/to/custom-transformer.js"}
Babelのサポート機能を使ってコードの変換処理を構築したい場合は、babel-jestを変換処理の作成に利用し、その中に独自の構成オプションを渡すことができます。
const babelJest = require('babel-jest');
module.exports = babelJest.createTransformer({
  presets: ['my-custom-preset'],
});
詳細は ドキュメント を参照してください。