create-react-appでReactのアプリを作り始めた場合、src/App.test.jpというファイル名で以下のようなテストが自動的に生成されます。
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
素晴らしいことに create-react-app コマンドでは package.json もいい感じに作ってくれるので、このテストを実行するにはいきなり npm test を実行するだけです。
しかし、この例では、App.js をマウントするときにエラーが無いかを確認しているだけなので、ちょっと頼りないですよね。
そこで AirBnBさん謹製の Enzyme を使用して、期待通りの要素があるかとか、クリックしたら期待通りに state が変わっているかとかをテストできるようにしてみたいと思います。
対象バージョン
Enzymeは、最新のメジャーバージョン 3.x で、APIの変更があったようなので、ここでは本日時点(2018/02/15) での最新版を対象としています。
- react-scripts - 1.1.1
- react - 16.2.0
- enzyme - 3.3.0
サンプル用のプロジェクトをつくる
create-react-app でプロジェクトをつくります。
$ create-react-app my-app
出来上がったプロジェクトのディレクトリに移動。
$ cd my-app
ローカルサーバーを使って出来上がったプロジェクトをブラウザで見てみましょう。
$ npm start
ここまでで準備は完了です。
Enzymeと関連するツールをインストールする
テストを実行するために npm testを実行するとreact-scripts testというコマンドが実行されますが、これは、ファイルの変更を監視して自動的にテストを走らせてくれます。
そのためにWatchmanというツールが必要なのでそれをインストールしましょう。
Homebrewであれば以下のような感じ。
$ brew install watchman
次に、テストに必要なnpmモジュールをインストールします。
$ npm install enzyme chai sinon enzyme-adapter-react-16 --save-dev
インストールするモジュールの中に chai がありますが、react-scripts test では、これはなくても大丈夫なんですが、Enzymeのドキュメントが chai ベースで書かれていて脳内翻訳がめんどくさいのでインストールしています。
次に src/setupTests.js というファイルをつくって以下のコードを記述します。
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
テストを書く
テストファイル App.test.js を以下のように修正して、必要なモジュールを require して、App.js で作成された <header /> という要素が期待通りにあるかどうかを確認します。
import React from 'react';
import ReactDOM from 'react-dom';
import { expect } from 'chai';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
import App from './App';
// const muiTheme = getMuiTheme();
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
it('<header /> should exist', () => {
const wrapper = shallow(<App />);
expect( wrapper.find('header.App-header').length ).to.equal(1)
});
以上でテストを実行してみましょう。
$ npm test
成功すると以下のような感じです。
PASS src/App.test.js
✓ renders without crashing (3ms)
✓ <header /> should exist (3ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.542s, estimated 1s
Ran all test suites.
Watch Usage: Press w to show more.
npm testを実行するとファイルの更新を監視して自動的にテストが実行されますので。さきほど修正した App.test.js にもう一つテストを追加してみます。
以下のコードを App.test.js の最後に追加して保存してください。
it('`.App-intro` should exist', () => {
const wrapper = shallow(<App />);
expect( wrapper.find('.App-intro').length ).to.equal(1)
});
するとテストが実行され以下のようになるはずです。
PASS src/App.test.js
✓ renders without crashing (3ms)
✓ <header /> should exist (2ms)
✓ `.App-intro` should exist (1ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.52s, estimated 1s
Ran all test suites.
Watch Usage: Press w to show more.
shallow() と mount()
追加したテストの中で shallow() という関数を使用して <App /> をマウントしていますが。Enzymeでは、この shallow() 以外にも mount() という関数でマウントする方法もあります。
shallow()を使用すれば、テスト対象のコンポーネント <App /> 内に子コンポーネントが記述されているときに、その子コンポーネントがマウントされずにテストが実行されます。
そうすることで、子コンポーネントの問題をテストから分離することが可能です。
一方で mount() は、子コンポーネントもマウントされて実行されますので、子コンポーネントの動作も含めたテストが可能になります。
Enzymeには、shallow()とmount()の他にもrender()という関数でコンポーネントをマウントする方法もありますが、これはDOMに対するテストだけなら便利そうですが、propsとかstateに対するテストをするにはいろいろ頑張らないといけなさそうなので、これを使用しないといけないケースはほとんど無い気がします。
クリックのテスト
では、App.jsを修正して React のロゴをクリックしたらテキストが表示されるようにしてみましょう。
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
state = {
welcome: "Welcome to React"
}
onClick = () => {
this.setState({welcome: 'Hello!'})
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" onClick={ this.onClick } />
<h1 className="App-title">{this.state.welcome}</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
npm startしてロゴをクリックすると以下のように Welcome to React が Hello に変わります。
次に App.test.js に以下のコードを追加します。
it('`Hello!` should be displayed', () => {
const wrapper = shallow(<App />);
wrapper.find('img').simulate('click');
expect( wrapper.find('.App-title').text() ).to.equal('Hello!')
});
ポイントは。find()に渡したセレクタでDOMを掘って見つけた結果に対して .simulate('click') を実行してるとことです。
