Testing React Apps
Facebook ではJestを使用して、Reactアプリケーションをテストします。
セットアップ
Create React Appを使用したセットアップ
Reactに馴染みがないなら、Create React Appの利用をお勧めします。 すぐに使えて Jestも同梱されています! スナップショットをレンダリングするには、 react-test-renderer
を追加するだけです。
実行
yarn add --dev react-test-renderer
Create React Appを使わないセットアップ
既存のアプリケーションがある場合は、いくつかのパッケージをインストールしてうまく機能するようにする必要があります。 babel-jest
パッケージと react
のbabel presetをテスト環境内のコードを変換するのに利用しています。 using babelも参照して下さい。
実行
yarn add --dev jest babel-jest babel-preset-env babel-preset-react react-test-renderer
package.json
は以下のようなものになっているはずです( <current-version>
はパッケージの実際の最新版のバージョンの数字になります)。 scriptsとJestの設定のエントリを追加して下さい:
// package.json
"dependencies": {
"react": "<current-version>",
"react-dom": "<current-version>"
},
"devDependencies": {
"babel-jest": "<current-version>",
"babel-preset-env": "<current-version>",
"babel-preset-react": "<current-version>",
"jest": "<current-version>",
"react-test-renderer": "<current-version>"
},
"scripts": {
"test": "jest"
}
// .babelrc
{
"presets": ["env", "react"]
}
それでは次へ進みましょう!
スナップショットテスト
ハイパーリンクをレンダリングするLinkコンポーネントの snapshot test を作成しましょう:
// Link.react.js
import React from 'react';
const STATUS = {
HOVERED: 'hovered',
NORMAL: 'normal',
};
export default class Link extends React.Component {
constructor(props) {
super(props);
this._onMouseEnter = this._onMouseEnter.bind(this);
this._onMouseLeave = this._onMouseLeave.bind(this);
this.state = {
class: STATUS.NORMAL,
};
}
_onMouseEnter() {
this.setState({class: STATUS.HOVERED});
}
_onMouseLeave() {
this.setState({class: STATUS.NORMAL});
}
render() {
return (
<a
className={this.state.class}
href={this.props.page || '#'}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
>
{this.props.children}
</a>
);
}
}
コンポーネントとのやり取りとレンダリングされた出力をキャプチャしてスナップショットファイルを作成するために、ReactのテストレンダラーとJestのスナップショット機能を利用しましょう:
// Link.react.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Link from '../Link.react';
test('Link 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
tree.props.onMouseEnter();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
// manually trigger the callback
tree.props.onMouseLeave();
// re-rendering
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
yarn test
または jest
を実行すると、このようなファイルが出力されます:
// __tests__/__snapshots__/Link.react.test.js.snap
exports[`Link changes the class when hovered 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}>
Facebook
</a>
`;
exports[`Link changes the class when hovered 2`] = `
<a
className="hovered"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}>
Facebook
</a>
`;
exports[`Link changes the class when hovered 3`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}>
Facebook
</a>
`;
次回のテストでは、レンダリングされた出力は前に作成されたスナップショットと比較されます。 The snapshot should be committed along with code changes. スナップショットテストが失敗した場合、それが意図的な変更かどうかを点検する必要があります。 変更が予想されたものであればjest -u
コマンドでJestを実行して既存のスナップショットを上書きします。
この例のコードはexamples/snapshotから利用可能です。
モック、Enzyme および React 16 を使用したスナップショットテスト
There's a caveat around snapshot testing when using Enzyme and React 16+. If you mock out a module using the following style:
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 triggers these warnings due to how it checks element types, and the mocked module fails these checks. Your options are:
Render as text. This way you won't see the props passed to the mock component in the snapshot, but it's straightforward: js jest.mock('./SomeComponent', () => () => 'SomeComponent');
Render as a custom element. DOM "custom elements" aren't checked for anything and shouldn't fire warnings. They are lowercase and have a dash in the name. tsx jest.mock('./Widget', () => () => <mock-widget />);
react-test-renderer
を使用する。 test renderer は、要素の型を気にしないので、SomeComponent
といった要素を許容してくれます。 test renderer を使うと、スナップショットをチェックしたり、Enzyme とは独立してコンポーネントのふるまいを確認することができます。警告をすべて無効にします(jestの設定ファイルで行う必要があります): js jest.mock('fbjs/lib/warning', () => require('fbjs/lib/emptyFunction')); これは、有用な警告が失われる可能性があるため、通常とるべき選択肢ではありません。 しかし、例えばreact-nativeのコンポーネントをテストする場合には、react-nativeタグをDOMにレンダリングする際に、多くの無関係な警告が発生します。 別の選択肢としては、console.warning を利用して、特定の警告を抑制することです。
DOM のテスト
レンダリングされたコンポーネントをアサートし操作したいのなら、react-testing-library、 Enzyme もしくは Reactの TestUtilsが利用できます。 以下の2つの例では、react-testing-library と Enzyme を使用します。
react-testing-library
react-testing-libraryを利用するには、 yarn add --dev @testing-library/react
を実行する必要があります。
2つのラベルを入れ替えるチェックボックスを実装しましょう。
// CheckboxWithLabel.js
import React from 'react';
export default class CheckboxWithLabel extends React.Component {
constructor(props) {
super(props);
this.state = {isChecked: false};
// bind manually because React class components don't auto-bind
// https://reactjs.org/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
this.onChange = this.onChange.bind(this);
}
onChange() {
this.setState({isChecked: !this.state.isChecked});
}
render() {
return (
<label>
<input
type="checkbox"
checked={this.state.isChecked}
onChange={this.onChange}
/>
{this.state.isChecked ? this.props.labelOn : this.props.labelOff}
</label>
);
}
}
// __tests__/CheckboxWithLabel-test.js
import React from 'react';
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 で入手できます。
Enzyme
You have to run yarn add --dev enzyme
to use Enzyme. If you are using a React version below 15.5.0, you will also need to install react-addons-test-utils
.
Let's rewrite the test from above using Enzyme instead of react-testing-library. この例ではEnxymeの shallow rendererを使用します。
// __tests__/CheckboxWithLabel-test.js
import React from 'react';
import {shallow} from 'enzyme';
import CheckboxWithLabel from '../CheckboxWithLabel';
test('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()).toEqual('Off');
checkbox.find('input').simulate('change');
expect(checkbox.text()).toEqual('On');
});
この例のコードはexamples/enzymeから利用可能です。
独自のコード変換処理
If you need more advanced functionality, you can also build your own transformer. Instead of using babel-jest
, here is an example of using babel-core
:
// custom-transformer.js
'use strict';
const babel = require('babel-core');
const jestPreset = require('babel-preset-jest');
module.exports = {
process(src, filename) {
if (babel.util.canCompile(filename)) {
return babel.transform(src, {
filename,
presets: [jestPreset],
retainLines: true,
}).code;
}
return src;
},
};
この例を動作させるには babel-core
とbabel-preset-jest
パッケージをインストールすることを忘れないで下さい。
これをJestと動作させるにはJestに次の設定を追加する必要があります:"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'],
});