create-react-app + Jest + Enzyme で書くReactコンポーネントテストの始め方
まえおき
- リファクタリングしたいのでまずはテストを用意しようと思いました
- Facebook謹製のJestというオールインワンなツールでテストをします
- Airbnb謹製のEnzymeというTest Utilitiesを使ってテストコードを書きます
- もとのWebアプリはcreate-react-appでつくられています
create-react-app + Jest + Enzyme でテストを書きはじめようと思ったときにしたこと、つまづいたことを書き留めておく回です。
Jestの公式サイト内にもcreate-react-appを使った解説が載っているので、公式派の人はそちらをどうぞ。
- まえおき
- Environments
- create-react-app のおさらい
- Jestとは、Enzymeとは
- テストの書き始め方
- Enzymeを使ってスナップショットのテストをする
- ハマったところ
- おわりに
Environments
- React 15.4.2
- create-react-app 0.8.5
- Jest 17.0.2
- Enzyme 2.8.2
での経験談を元にしています。
create-react-app のおさらい
コマンド一発でReactの開発環境をつくることができるツールです。
(参考:Reactを秒速で使い始められるcreate-react-appの使い方と使い心地)
これを使って構築した環境には標準でJestが組み込まれていて、yarn test
コマンドですぐにテストを実行することができます。
Create React App uses Jest as its test runner. To prepare for this integration, we did a major revamp of Jest so if you heard bad things about it years ago, give it another try.
create-react-app/README.md at master · facebookincubator/create-react-app · GitHubより
Jestとは、Enzymeとは
Jest
- Jest · 🃏 Delightful JavaScript Testing
- Reactのテストをするためのツールです
- 単体テストを書くためのfunction群と、テストを実行するテストランナーが含まれています(オールインワン)
Enzyme
- Enzyme
- Reactのテストコードを書くためのUtilityです
- Jestだけではできない高度なテストを書くことができるfunction群です(Jestと重なる部分もあります)
- Reactコンポーネントをrenderingする
shallow
mount
render
の3つのfunctionがキモです
テストの書き始め方
1. テストファイルの配置場所を知る
Jestは任意のディレクトリからテストコードを探し出し順に実行してくれます。本来このディレクトリはpackage.json
にrootDir
という名前で定義するものなのですが、create-react-appでつくられた環境ではsrc/
と決められています。
// react-scripts/utils/createJestConfig.js
module.exports = (resolve, rootDir, isEjecting) => {
// (省略)
if (rootDir) {
config.rootDir = rootDir; // ← ここで引数をもとにrootDirを指定している
}
return config;
};
// react-scripts/scripts/test.js
argv.push('--config', JSON.stringify(createJestConfig(
relativePath => path.resolve(__dirname, '..', relativePath),
path.resolve(paths.appSrc, '..'), // ← ここで引数rootDirを指定している
false
)));
したがって、src/
にテストファイルを配置していきます。
2. テストファイルを作成する
Jestにテストファイルを認識させるには2つの方法があります。
__test__
というディレクトリ以下にテストファイルを置く.test.js
という拡張子のファイルを作成する
私は②を採用しました。
src
├ actions
├ components
︙ ├ CheckboxWithLabel.js (Buttonコンポーネント)
└ test
└ CheckboxWithLabel.test.js (Buttonコンポーネントのテスト)
3. テストコードを書く
Jestの各functionの使い方はこの記事が分かりやすかったです。 Facebook製のJavaScriptテストツール「Jest」の逆引き使用例 - Qiita
ここでは、「チェックボックスのOn/Offに応じてラベルの文言が切り替わるコンポーネント」のテストの実装例を挙げます。ソースを読んで雰囲気がつかめるかと思います(がいかがでしょう)。
src/components/CheckboxWithLabel.js
import React, { Component } from 'react';
export default class CheckboxWithLabel extends Component {
constructor(props) {
super(props);
this.state = {isChecked: false};
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>
);
}
}
src/components/test/CheckboxWithLabel.test.js
import React from 'react';
import { shallow, mount } from 'enzyme';
import CheckboxWithLabel from '../CheckboxWithLabel';
describe('ChecboxWithLabel', () => {
test('Changes the label after click', () => {
// Componentをレンダリングする
const checkbox = shallow(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
// expect(検査対象).toEqual(想定結果)
expect(checkbox.text()).toEqual('Off');
// shallowでレンダリングされた要素から特定のセレクタを取得する
checkbox.find('input').simulate('change');
// expect(検査対象).toEqual(想定結果)
expect(checkbox.text()).toEqual('On');
});
});
ここでつかったshallow
という関数は、その名の通りComponentを浅くレンダリングします。Componentの中にComponentがある入れ子構造の場合、一番外側のComponentしかレンダリングされません。子コンポーネントに依存していない結果を検査することができます。
Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren’t indirectly asserting on behavior of child components.
Enzymeを使ってスナップショットのテストをする
ここからはJest + Enzymeの話です。
これらを組み合わせることで、Componentのレンダリング結果をスナップショットとして残しておき、それと比較してレンダリング結果に差異がないかのテストをすることができます。
上記のCheckboxWithLabelでその例を書いてみます。
describe('CheckboxWithLabel', () => {
test('Matches with the snapshot when checkbox is OFF', () => {
const dom = mount(<CheckboxWithLabel labelOn="On" labelOff="Off" />);
expect(dom).toMatchSnapshot();
});
});
toMatchSnapshot()
は、既存のスナップショットと比較してレンダリング結果が一致しているかどうかを判定します。最初の一回目はsnapshotがないため必ずpassします。snapshotを更新する場合はjest --update
またはjest -u
コマンドを叩きます。
ここで使ったmount
という関数は、Componentを完全にレンダリングします。その分テストの実行速度が遅くなるため、入れ子の深いComponentをmountしたり、mountを使ったテストをたくさん書くとなかなかテストが終わりません。
その場合はテストを分割して行うなどの工夫が必要でした。
Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs, or may require the full lifecycle in order to fully test the component (i.e., componentDidMount etc.)
ハマったところ
🙅localStorage is not defined
テストするComponentが依存しているあるモジュールは、内部でlocalStorageを利用していました。そのためimport時にlocalStorage is not defined
のエラーが起きました。
Jestはテスト実行前にテスト環境を構築するコードを実行することができるので、それを利用してlocalStorageモックを定義することで対処できます。一般的には、package.json
にsetupFiles
またはsetupTestFrameworkScriptFile
として定義したファイルがテスト前に実行されます。
(参考: Configuring package.json · Jest)
"jest": {
"setupFiles": ["createLocalStorageMock.js"]
}
ただし、create-react-appを使っている場合はやはりこの設定が効かず、独自の設定ファイルsrc/setupTests.js
にモックを書く必要がありました。
src/setupTests.js
const localStorageMock = (() => {
var storage = {};
return {
setItem: (key, value) => {
storage[key] = value || '';
},
getItem: (key) => {
return storage[key] || null;
},
removeItem: (key) => {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key: (i) => {
var keys = Object.keys(storage);
return keys[i] || null;
}
};
})();
Object.defineProperty(global, 'localStorage', { value: localStorageMock });
🙅ReactTestUtils has been moved to react-dom/test-utils.
テスト結果に以下のワーニング文が表示されていました。
Warning: ReactTestUtils has been moved to react-dom/test-utils. Update references to remove this warning.
ReactTestUtilsの依存元が変わったってことだと思いReact公式を調べたところ、以下の対応をするように記載がありました。
import ReactTestUtils from 'react-dom/test-utils'; // ES6
var ReactTestUtils = require('react-dom/test-utils'); // ES5 with npm
言われるがままにためしたものの、Cannot find module 'react-dom/test-utils’
と怒られる。いかがなものですかね。
ということで、今度はワーニング文のUpdate references
にバカ正直に従い依存モジュールを丸々アップデートしてみました。
$ yarn cache clean
$ yarn upgrade
解決しちゃいました。
おわりに
リファクタしてまるっと書き直したい部分があったのですが、デグレしない保証をとる手段がなさそうだったのでひとまずテストを用意しよう! ということで書き始めたテストですが(遅い)、思った以上に融通が効くし書き心地が良くて充実したテストコーディングとなりました。とくにレンダリング結果をsnapshotで比較できるのは重宝しそうな感じです。
そんな感じでJestとEnzymeはよかったものの、create-react-appは便利な反面制約が多くハマりどころが多々あるので、ハマったときはこうして記録を残していこうと思います。
それでは。