ReactのテストをEnzymeで書いてみよう

GithubのExploreを覗いていたら面白そうなテストツールがありました。
https://github.com/airbnb/enzyme

Reactに力を入れているというAirbnbが作ったテスティングツールなのですが、ドキュメントがサラッとしか書いてなかったのでお遊びがてら使えるようになるまで触った軌跡を書いてきます。

「んなことより早くブツを見せんかい！！」という猛者JSerな方は以下のリポジトリから何となく意図を汲みとって何となくマサカリを投げてください。

https://github.com/syossan27-sandbox/enzyme-sandbox

## はじめに

まずは必要なものをnpmで入れてきましょう。

```
$ npm install -g mocha
$ npm install --save-dev chai
$ npm install --save-dev babel-register
$ npm install --save-dev enzyme
```

インストールしたものはそれぞれ

- mocha（テスティングフレームワーク）
- chai（アサーションライブラリ）
- babel-register（importしたライブラリを実行前にトランスパイルしてくれる）
- enzyme（今回の主役）

ちなみに今回はmocha・chaiでやってますが別にこれじゃないと出来無い訳ではないのでお好みで。

次にnpm-scriptでbuildとtestを担うスクリプトを作成しましょう。

```package.json
  "scripts": {
    "build": "browserify --debug index.jsx --outfile bundle.js -t [ babelify --presets [ es2015 react ] ]",
    "test": "mocha --compilers js:babel-register"
  }
```

buildの方は、まずbabelifyでindex.jsxをトランスパイルしてその結果をbundle.jsとしてbrowserifyで出力しています。
testはmochaを使い、jsはbabel-registerを用いて実行するようにしています。

## ファイルを用意しよう

次に諸々の必要なファイルを用意しましょう。
今回は以下の様なディレクトリ構成でやっています。

```
% tree -L 1
.
├── Foo.jsx　（Fooコンポーネントが詰まったファイル）
├── My.jsx　（Myコンポーネントが詰まったファイル）
├── README.md
├── bundle.js　（ビルド後のファイル）
├── index.html　（表示用HTML）
├── index.jsx　（コンポーネントを取りまとめるためのファイル）
├── node_modules
├── package.json
└── test　（テストが詰まったディレクトリ）
```

それでは、Foo.jsx・My.jsx・index.jsxをそれぞれ作成しましょう。

```Foo.jsx
import React from 'react'
import ReactDOM from 'react-dom'

let Foo = React.createClass({
  render: () => {
    return (
      <p>こんにちは！</p>
    )
  }
})

export default Foo;
```

```My.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import Foo from './Foo.jsx'

let MyComponent = React.createClass({
  render: () => {
    return (
      <div>
        <h1>Hello, World!</h1>
        <Foo />
        <Foo />
        <Foo />
      </div>
    )
  }
})

export default MyComponent;
```

```index.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import MyComponent from './My.jsx'

ReactDOM.render(
  <MyComponent />,
  document.getElementById('contents')
)
```

FooコンポーネントとMyコンポーネントは、最後に `export default xxx;` を使ってimportされた時にコンポーネントが呼び出されるようにしています。
そしてindex.jsxはMyコンポーネントを、MyコンポーネントではFooコンポーネントを順に呼び出しています。

それではテストファイルを作りましょう。

```
$ mkdir test
```

```test/test.js
import React from 'react'
import ReactDOM from 'react-dom'
import { shallow, mount, render } from 'enzyme'
import { assert, expect } from 'chai'
import MyComponent from '../My.jsx'
import Foo from '../Foo.jsx'

describe('<MyComponent />', () => {

  it('should render three <Foo /> components', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find(Foo)).to.have.length(3);
  });

  it('should render an `.icon-star`', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find('.icon-star')).to.have.length(1);
  });

  it('should render children when passed in', () => {
    const wrapper = shallow(
      <MyComponent>
        <div className="unique" />
      </MyComponent>
    );
    expect(wrapper.contains(<div className="unique" />)).to.be.true;
  });

  it('simulates click events', () => {
    const onButtonClick = sinon.spy();
    const wrapper = shallow(
      <Foo onButtonClick={onButtonClick} />
    );
    wrapper.find('button').simulate('click');
    expect(onButtonClick.calledOnce).to.be.true;
  });

});
```

ほぼ[例の通り](http://airbnb.io/enzyme/#-shallow-rendering-docs-api-shallow-md)にやりました。
今回はコンポーネントの階層が浅いのでshallow renderingでテストを書きましたが、
コンポーネント階層を深くまでテストをしたい場合は、JSDOM Full Renderingを用いてください。

また、テストの記述方法ですが__describe__ではどのコンポーネントに対してテストを行うかを設定していて、__it__ではそのテストの内容を書いていきます。
RSpecのような書き方が出来るのは魅力的ですね。

ちなみに今回は１つ目のテストだけパスするように作ってあります。

## テストの実行

それでは実際にテストを実行してみましょう。

```
$ npm run test
> enzyme-sandbox@1.0.0 test /Users/hoge/enzyme-sandbox
> mocha --compilers js:babel-register

  <MyComponent />
    ✓ should render three <Foo /> components
    1) should render an `.icon-star`
    2) should render children when passed in
    3) simulates click events


  1 passing (124ms)
  3 failing

  1) <MyComponent /> should render an `.icon-star`:
     AssertionError: expected { Object (root, unrendered, ...) } to have a length of 1 but got 0
      at Context.<anonymous> (test.js:17:48)

  2) <MyComponent /> should render children when passed in:
     AssertionError: expected false to be true
      at Context.<anonymous> (test.js:26:63)

  3) <MyComponent /> simulates click events:
     ReferenceError: sinon is not defined
      at Context.<anonymous> (test.js:30:27)
```

こんな感じの結果になりました。
見た通り、１つ目のテストだけパスしてますね。

とりあえず、ザッとこんな感じで動かすことが出来ました。
ReactのテストをRSpecのように整然と書くことが出来るのはなかなか便利ですね。
Airbnbが開発のため、これからの積極的なアップデートも楽しみです。

今回はこのような形でテストを行いましたが、「もっとこんな形でしたほうがいいよ」という方がおりましたらコメント欄等でのご指摘お願い致します。