React アプリのテスト
Facebookでは、React アプリケーションのテストにJestを使用しています。
セットアップ
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プリセットを使用しています。 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>`はパッケージの実際の最新バージョン番号です)。スクリプトと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コンポーネントのスナップショットテストを作成してみましょう
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: 関数コンポーネントとクラスコンポーネントを参照してください。クラスコンポーネントでは、Jestを使用してpropsをテストし、メソッドを直接テストしないことを想定していることに**注意してください**。
それでは、ReactのテストレンダラーとJestのスナップショット機能を使用して、コンポーネントを操作し、レンダリングされた出力をキャプチャして、スナップショットファイルを作成してみましょう
import renderer from 'react-test-renderer';
import Link from '../Link';
it('changes the class when hovered', () => {
const component = renderer.create(
<Link page="https://#">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="https://#"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
exports[`changes the class when hovered 2`] = `
<a
className="hovered"
href="https://#"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
exports[`changes the class when hovered 3`] = `
<a
className="normal"
href="https://#"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
次回テストを実行すると、レンダリングされた出力が以前に作成されたスナップショットと比較されます。スナップショットはコードの変更と一緒にコミットする必要があります。スナップショットテストが失敗した場合は、それが意図した変更か、意図しない変更かを調べる必要があります。変更が予期される場合は、`jest -u`を使用して既存のスナップショットを上書きできます。
この例のコードは、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.
# Or:
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は表示されませんが、簡単です
jest.mock('./SomeComponent', () => () => 'SomeComponent');
- カスタム要素としてレンダリングする。DOMの「カスタム要素」は何もチェックされず、警告は発生しません。小文字で、名前にダッシュが含まれています。
jest.mock('./Widget', () => () => <mock-widget />);
- `react-test-renderer`を使用する。テストレンダラーは要素タイプを気にせず、`SomeComponent`などを喜んで受け入れます。テストレンダラーを使用してスナップショットをチェックし、Enzymeを使用してコンポーネントの動作を個別にチェックできます。
- 警告をすべて無効にする(jest設定ファイルで行う必要があります)
有用な警告が失われる可能性があるため、通常はこれは選択すべきオプションではありません。ただし、react-nativeのコンポーネントをテストする場合など、react-nativeタグをDOMにレンダリングしており、多くの警告が無関係な場合があります。別のオプションは、`console.warn`をスウィズルして特定の警告を抑制することです。
jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction'));
DOMテスト
レンダリングされたコンポーネントをアサートおよび操作する場合は、@testing-library/react、Enzyme、またはReactのTestUtilsを使用できます。次の例では、`@testing-library/react`を使用しています。
@testing-library/react
- 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();
});
この例のコードは、examples/react-testing-libraryにあります。
カスタムトランスフォーマー
より高度な機能が必要な場合は、独自のカスタムトランスフォーマーを構築することもできます。`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`を使用して1つを作成し、カスタム設定オプションを渡すこともできます
const babelJest = require('babel-jest');
module.exports = babelJest.createTransformer({
presets: ['my-custom-preset'],
});
詳細については、専用のドキュメントを参照してください。