Jest と ReactTestUtils で React Component のユニットテストを書く


facebook製のテストフレームワーク[Jest](https://facebook.github.io/jest/)を使ってReact Component のユニットテストを書く方法について簡単にまとめました。

[こちら](https://github.com/kjugk/Jest-example)に今回使ったサンプルコードをアップしましたので、合わせてご参照ください!

## 前提
* jest-cli v0.9.2
* babel-jest v9.0.3
* react-addons-test-utils v0.14.7
* react v0.14.7
* babel で jsx, es6 をコンパイルする

## Jest の主な特徴
* テストコード内で require した module は、自動的に mock 化される(後述)。[https://facebook.github.io/jest/docs/automatic-mocking.html](https://facebook.github.io/jest/docs/automatic-mocking.html)

* デフォルトで [jasmine 2](http://jasmine.github.io/2.0/introduction.html)を利用している。


## 設定

### module のインストール
Jest でテストを実行するために必要な下記 module をインストールします。

```
$ npm install --save-dev jest-cli babel-jest react-addons-test-utils
```

### package.json 
* scripts.test に jest を指定して、npm test コマンドでテストを実行できるようにします。
* 必須 module を 自動的に mock 化させないために、jest.unmockedModulePathPatterns にreact, react-dom, react-addons-test-utilsを 指定します。

```:package.json
"scripts": {
  "test": "jest"
}

"jest": {
  "unmockedModulePathPatterns": [
    "<rootDir>/node_modules/react",
    "<rootDir>/node_modules/react-dom",
    "<rootDir>/node_modules/react-addons-test-utils"
  ]
}
```

##テスト実行
以上の設定により、npm test で test が走るようになります!

```
$ npm test
> Jest-example@0.0.1 test [app_dir]/Jest-example
> jest

Using Jest CLI v0.9.2, jasmine2, babel-jest
No tests found for "". Regex used while searching: /.*/.
```

当然ですが、まだテストコードが存在しないため、テストは実行されません。
では、早速 Component のテストを書いてみましょう！

## テストを書く
### どこに?
テストコードは、`__tests__` ディレクトリに配置していきます。
Jest が 自動的に `__tests__`　ディレクトリ内のテストを探して実行してくれるので、テストを一つのディレクトリにまとめても、各サブディレクトリに分割して配置してもOKです。

ファイル名は、[ModuleName]-test.js というルールで作成していきます。
### 何を?
今回テストするのは、下記のような単純なButton コンポーネントです。

```:js/components/Button.js
'use strict'
import React, { Component, PropTypes } from 'react'

class Button extends Component{
  render(){
    const{ onClick, labelText, disabled } = this.props;

    return(
      <input type="button" value={labelText} onClick={onClick.bind(this)} disabled={disabled}/>
    )
  }
}

Button.propTypes = {
  onClick: PropTypes.func.isRequired,
  labelText: PropTypes.string.isRequired,
  disabled: PropTypes.bool
}
Button.defaultProps = { disabled: false };

export default Button

```

仕様は下記の通りです。

* property として、onClick(必須), labelText(必須), disabled(任意) を受け取る。
* button が クリックされたら、onClick property を実行する。
* labelText property を、buttonの value 属性にセットする。
* disabled property を、button のdisabled 属性にセットする。
* disabled の デフォルト値は false.

### どうやって?
上記仕様をテストするコードを、下記のように記述しました。(解説は後述)

```:js/components/__tests__/Button-test.js
'use strict'

jest.unmock('../Button')

import React from 'react'
import ReactDOM from 'react-dom'
import TestUtils from 'react-addons-test-utils'
import Button from '../Button'

describe('Button', () => {
  let props ={
    labelText : "hoge",
    onClick : jest.fn()
  }

  function setup(){
    const instance = TestUtils.renderIntoDocument(
      <Button {...props} />
    )
    const buttonNode = ReactDOM.findDOMNode(instance)
    return {
      instance,
      buttonNode
    }
  }

  describe('click', () =>{
    it('calls onClick props', () => {
      const {buttonNode} = setup()

      TestUtils.Simulate.click(buttonNode)
      expect(props.onClick).toBeCalled()
    })
  })

  describe('value', () =>{
    it ('should be equal to label props', ()=>{
      props.labelText = "foo"
      const {buttonNode} = setup()

      expect(buttonNode.value).toEqual("foo")
    })
  })

  describe('disabled', () =>{
    it ('shoudl be false by default', ()=>{
      const {buttonNode} = setup()
      expect(buttonNode.disabled).toBeFalsy()
    })

    it ('should be equal to disabled props', ()=>{
      props.disabled = true
      const {buttonNode} = setup()

      expect(buttonNode.disabled).toBeTruthy()
    })
  })
})

```

###重要なポイント
#### テスト対象 module の mock 化を回避する

3行目で、テスト対象module を unmockしています。

```javascript
jest.unmock('../Button')
```

jest では、テストコード内の　require の戻り値は自動的に mock object になるため、実際のmodule　が必要な場合は、必ず unmock する必要があります。


---
#### テスト対象 Component を DOM に render する

setup 関数の中で、Button Component を DOM にrender しています。

```
const instance = TestUtils.renderIntoDocument(
  <Button {...props} />
)
```

---
#### render された DOM を取得する
今回のケースでは、Button Component は 一つの input 要素 を render しているだけなので、ReactDOM.findDOMNode を使って DOM を取得しています。

```
const buttonNode = ReactDOM.findDOMNode(instance)
```


ReactTestUtils には 他にもDOM を取得する function が定義されているので、ケースによって使い分けていきましょう。
[https://facebook.github.io/react/docs/test-utils.html](https://facebook.github.io/react/docs/test-utils.html)

----
#### event の シミュレート
ボタンクリック時の仕様をテストするために、ReactTestUtils.Simulate を使って、click インベントを emit しています。

```
describe('click', () =>{
  it('calls onClick props', () => {
    const {buttonNode} = setup()

    TestUtils.Simulate.click(buttonNode)
    expect(props.onClick).toBeCalled()
  })
})
```

[React で 使える event](https://facebook.github.io/react/docs/events.html#supported-events) は全てシミュレートできます。


## 再びテスト実行
```
$ npm test
> Jest-example@0.0.1 test [app_dir]/Jest-example
> jest

Using Jest CLI v0.9.2, jasmine2, babel-jest
 PASS  js/components/__tests__/Button-test.js (1.035s)
4 tests passed (4 total in 1 test suite, run time 1.814s)
```

テストが通りました!


## 使ってみて
* 「自動的に依存モジュールが unmock される」仕様に慣れてしまえば、かなりシンプルにテストを記述できそう。=> とりあえずユニットテストは Jest に絞っていろいろ書いてみる。

* どこまでComponentのテストを書くかは、まだ模索中。実践しながら落としどころを探っていく必要がある。ここまで書いといてなんだけど、今回の様な粒度でのComponentのテストは、必要ないのかもしれない。
* flux では store のテストを書くのが再重要? => 調べて記事にする。

## 今後の課題
* 非同期関数のテストを書く。
* flux, redux のテストについてまとめる。