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/preset-env": "<current-version>",
"@babel/preset-react": "<current-version>",
"babel-jest": "<current-version>",
"jest": "<current-version>",
"react-test-renderer": "<current-version>"
},
"scripts": {
"test": "jest"
}
// babel.config.js
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-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 を使用したスナップショットテスト
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の設定ファイルで行う必要があります): 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
.
react-testing-libraryの代わりにEnzymeを使用して上記のテストを書き直しましょう。この例ではEnzymeの 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 {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"}
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.