Edited at

Enzyme 3.x で `create-react-app` で作成した React コンポーネントのユニットテスト

More than 1 year has passed since last update.

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 が変わっているかとかをテストできるようにしてみたいと思います。

http://airbnb.io/enzyme/


対象バージョン

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 ReactHello に変わります。

次に 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') を実行してるとことです。


参考