React Tutorial Example (Jest)


[React](https://facebook.github.io/react/) や [Redux](http://redux.js.org/) は [Jest](http://facebook.github.io/jest/) という JavaScript の Testing Framework が利用されています。
Actions, Reducers のテストはもちろん、Components のテストもできるので便利です。
[前回](http://qiita.com/ogomr/items/224a1355078ea1a809a5) のアプリケーションに *テスト* を実装してみましょう。

## Learning

* Jest で *Actions*, *Reducers* のテストをする。
* Enzyme で *Components* のテストをする。

## Environment

* node: v6.9.1
* npm: v4.0.3
* yarn: v0.18.0

## Comment Box Form

* 完成される [Source Code](https://github.com/ogom/react-comment-box-example/tree/jest) のファイルリストです。

```bash
$ tree -a -I node_modules
.
├── .babelrc
├── app
│   ├── actions
│   │   └── index.js
│   ├── components
│   │   ├── Comment
│   │   │   └── index.js
│   │   ├── CommentBox
│   │   │   ├── index.js
│   │   │   └── style.css
│   │   ├── CommentForm
│   │   │   └── index.js
│   │   ├── CommentList
│   │   │   └── index.js
│   │   └── global.css
│   ├── containers
│   │   └── App
│   │       ├── index.js
│   │       └── style.css
│   ├── index.js
│   ├── reducers
│   │   └── index.js
│   └── store
│       └── index.js
├── test
│   └── app
│       ├── actions
│       │   └── index.test.js
│       ├── components
│       │   ├── Comment
│       │   │   └── index.test.js
│       │   └── CommentBox
│       │       └── index.test.js
│       └── reducers
│           └── index.test.js
├── index.html
├── package.json
├── webpack.config.js
└── yarn.lock
```

*Let's hands-on*

### Setup application

* `git clone` コマンドでアプリケーションをダウンロードします。
* `yarn` コマンドで依存するモジュールをインストールします。

```bash
$ git clone https://github.com/ogom/react-comment-box-example.git
$ cd react-comment-box-example
$ git checkout package
$ yarn
```

## Jest


* Jest のモジュールをインストールします。
	* [redux の Writing Tests](https://github.com/reactjs/redux/blob/master/docs/recipes/WritingTests.md) を参考にしました。

```bash
$ yarn add --dev jest babel-jest
```

* `package.json` の `scripts` に `test` を記述します。 

```diff:package.json
   "scripts": {
     "start": "webpack-dev-server -d --progress --colors",
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "test": "jest",
+    "test:watch": "npm test -- --watch"
   },
```

* `yarn test` コマンドでテストを実行します。
	* まだテストを書いていないので、`No tests found` が表示されます。

```bash
$ yarn test
No tests found
```

* `yarn run test:watch` コマンドでファイルの変更を監視します。

```bash
$ yarn run test:watch
No tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.

Watch Usage
 › Press a to run all tests.
 › Press o to only run tests related to changed files.
 › Press p to filter by a filename regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
```

それでは、監視した状態でテストを書いてみましょう！

### Actions

* `describe` に *概要や機能* を記述します。`it` には *期待* を記述します。
	* `expect` に *観察の対象* を記述します。`toEqual` に *期待の結果* を記述します。
* `expect` に **Action** を記述して `toEqual` に *Create* される `type` や `payload` を記述します。

```javascript:test/app/actions/index.test.js
import * as actions from '../../../app/actions'

describe('actions', () => {
  it('should create an action to show the comments', () => {
    const value = []
    const expectedAction = {
      type: 'show_comments',
      payload: value
    }
    expect(
      actions.showComments(value)
    ).toEqual(
      expectedAction
    )
  })

  it('should create an action to add a comment', () => {
    const value = {foo: '123', bar: '456'}
    const expectedAction = {
      type: 'add_comment',
      payload: value
    }
    expect(
      actions.addComment(value)
    ).toEqual(
      expectedAction
    )
  })
})
```

* テストを実行すると `describe` と `it` が出力されます。 

```bash
 PASS  test/app/actions/index.test.js
  actions
    ✓ should create an action to show the comments (4ms)
    ✓ should create an action to add a comment (1ms)
```

### Reducers

* `expect` に **Reducer** を記述して `toEqual` に *State* を記述します。 
	* **Action** の実行で *State* の変更を確認できる、このテストは大切です。 

```javascript:test/app/reducers/index.test.js
import * as actions from '../../../app/actions'
import reducers from '../../../app/reducers'

describe('reducers', () => {
  let state
  beforeEach(() => {
    state = [undefined]
  })

  it('should return the initial state', () => {
    expect(
      reducers(undefined, {type: ''})
    ).toEqual(
      state
    )
  })

  it('should return the show comments', () => {
    const value = [{foo: '123', bar: '456'}]
    state = value
    expect(
      reducers(undefined, actions.showComments(value))
    ).toEqual(
      state
    )
  })

  it('should return the add a comment', () => {
    const value = {foo: '123', bar: '456'}
    state = [value]
    expect(
      reducers(undefined, actions.addComment(value))
    ).toEqual(
      state
    )
  })
})
```

### Components

* Enzyme のモジュールをインストールします。
	* [Jest Tutorial - webpack](https://facebook.github.io/jest/docs/tutorial-webpack.html) を参考にしました。

```bash
$ yarn add --dev enzyme react-addons-test-utils
```

#### Comment

* `expect` に **Component** を記述して `toBe` に *期待値* を記述します。 

```javascript:test/app/components/Comment/index.test.js
import React from 'react'
import { shallow } from 'enzyme'
import Comment from '../../../../app/components/Comment'

describe('components', () => {
  describe('Comment', () => {
    const wrapper = shallow(<Comment author={'foo'}>{'bar'}</Comment>)
    it('should return author and comment', () => {
      expect(wrapper.find('.commentAuthor').text()).toBe('foo')
      expect(wrapper.find('span').render().find('p').text()).toBe('bar')
    })
  })
})
```

#### CommentBox

* *CSS Modules* は `identity-obj-proxy` でモックを利用します。

```bash
$ yarn add --dev identity-obj-proxy
```

* `package.json` の `jest` に `moduleNameMapper` を記述します。 

```diff:package.json
+  "jest": {
+    "moduleNameMapper": {
+      "\\.css$": "identity-obj-proxy"
+    }
+  },
```

* *CSS* を含む **Component** のテストも実行できます。

```javascript:test/app/components/CommentBox/index.test.js
import React from 'react'
import { shallow } from 'enzyme'
import CommentBox from '../../../../app/components/CommentBox'

describe('components', () => {
  describe('CommentBox', () => {
    const wrapper = shallow(<CommentBox/>)
    it('should return the Comments', () => {
      expect(wrapper.find('h1').text()).toBe('Comments')
    })
  })
})
```

* 最後に `yarn test` で全て *PASS* するか確認しましょう。

```bash
$ yarn test
 PASS  test/app/components/Comment/index.test.js
 PASS  test/app/components/CommentBox/index.test.js
 PASS  test/app/reducers/index.test.js
 PASS  test/app/actions/index.test.js

Test Suites: 4 passed, 4 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        1.461s
Ran all test suites.
✨  Done in 1.96s.
```

*Congrats!*

----

テストは設計フェーズや保守フェーズで、利用の方法によってはとても便利な機能です。
次回は [electron で native application](http://qiita.com/ogomr/items/681f5bffe9908e24469a) にしましょう！