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

`create-react-app`でReactのアプリを作り始めた場合、`src/App.test.jp`というファイル名で以下のようなテストが自動的に生成されます。

```javascript
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](https://facebook.github.io/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` というファイルをつくって以下のコードを記述します。

```javascript
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });
```

## テストを書く

テストファイル `App.test.js` を以下のように修正して、必要なモジュールを `require` して、`App.js` で作成された `<header />` という要素が期待通りにあるかどうかを確認します。

```javascript
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` の最後に追加して保存してください。

```javascript
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()` は、子コンポーネントもマウントされて実行されますので、子コンポーネントの動作も含めたテストが可能になります。

* [Shallow Rendering API](http://airbnb.io/enzyme/docs/api/shallow.html)
* [Full Rendering API](http://airbnb.io/enzyme/docs/api/mount.html)

Enzymeには、`shallow()`と`mount()`の他にも`render()`という関数でコンポーネントをマウントする方法もありますが、これはDOMに対するテストだけなら便利そうですが、`props`とか`state`に対するテストをするにはいろいろ頑張らないといけなさそうなので、これを使用しないといけないケースはほとんど無い気がします。

## クリックのテスト

では、`App.js`を修正して React のロゴをクリックしたらテキストが表示されるようにしてみましょう。

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

![](https://www.evernote.com/l/ABWBgTJRPe1CN5Tf3QAfpdRBarRLqLT5CCcB/image.png)

次に `App.test.js` に以下のコードを追加します。

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

## 参考

* [Running Tests](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#running-tests)
* [Shallow Rendering API](http://airbnb.io/enzyme/docs/api/shallow.html)
* [Full Rendering API](http://airbnb.io/enzyme/docs/api/mount.html)