Reactアプリをテスト
At Facebook, we use Jest to test React applications.
セットアップ
既存のアプリケーションがある場合は、いくつかのパッケージをインストールしてうまく機能するようにする必要があります。 We are using the babel-jest package and the react babel preset to transform our code inside of the test environment. Also see using babel.
実行
- npm
- Yarn
- pnpm
- Bun
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
bun add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer
Your package.json should look something like this (where <current-version> is the actual latest version number for the package). 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'}],
],
};
And you're good to go!
スナップショットテスト
Let's create a snapshot test for a Link component that renders hyperlinks:
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>
);
}
例として関数コンポーネントを使用していますが、クラスコンポーネントも同じ方法でテストすることができます。 See React: Function and Class Components. Reminders that with Class components, we expect Jest to be used to test props and not methods directly.
コンポーネントとのやり取りとレンダリングされた出力をキャプチャしてスナップショットファイルを作成するために、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();
});
When you run yarn test or jest, this will produce an output file like this:
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>
`;
次回のテストでは、レンダリングされた出力は前に作成されたスナップショットと比較されます。 スナップショットは、コードの変更に沿ってコミットされるべきです。 スナップショットテストが失敗した場合、それが意図的な変更かどうかを点検する必要があります。 If the change is expected you can invoke Jest with jest -u to overwrite the existing snapshot.
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.
# 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 />);
- Use
react-test-renderer. The test renderer doesn't care about element types and will happily accept e.g.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 @testing-library/react, Enzyme, or React's TestUtils. The following example use @testing-library/react.
@testing-library/react
- npm
- Yarn
- pnpm
- Bun
npm install --save-dev @testing-library/react
yarn add --dev @testing-library/react
pnpm add --save-dev @testing-library/react
bun add --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/[email protected] 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.
独自のコード変換処理
より高度な機能が必要な場合は、独自の変換処理を構築することもできます。 Instead of using babel-jest, here is an example of using @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;
},
};
Don't forget to install the @babel/core and babel-preset-jest packages for this example to work.
To make this work with Jest you need to update your Jest configuration with this: "transform": {"\\.js$": "path/to/custom-transformer.js"}.
If you'd like to build a transformer with babel support, you can also use babel-jest to compose one and pass in your custom configuration options:
const babelJest = require('babel-jest');
module.exports = babelJest.createTransformer({
presets: ['my-custom-preset'],
});
See dedicated docs for more details.