React.js の 公式Tutorial を Redux を利用して書き直した。また Mocha + power-assert を利用したテストも追加


前回 Qiitaに[React.js の React.js の公式Tutorial を gulp を利用して簡単に実行できる環境を作って、ES6も試した
](http://qiita.com/ma-tu/items/9d58f5db7e7afd306dd7)で公式TutorialをECMA2015(ECMA6)を利用して書き直しを行ったのですが、ReactといえばFlux、Flux といえばRedux ということで、流行りのRedux を利用して記述したらどうなるのかを試してみました。

また 同じく流行りのテスト環境である Mocha + power-assert を利用して、テストを追加してみました。
React および Redux はテストも非常に行いやすいのを体感しました。

前回の続編として、以下GitHub で公開しています。
https://github.com/ma-tu/react-babel-gulp-browserify-tutorial/tree/es6-redux

React.js の version は 0.14.5 で Redux の version は 3.0.5 となります。
2016年1月時点のライブラリで作成しています。Windows 環境で確認しています。

## Redux について

以下の和訳した情報が非常に読みやすくて参考になります。私がつたない文章で説明するよりも、**[Redux入門【ダイジェスト版】10分で理解するReduxの基礎](http://qiita.com/kiita312/items/49a1f03445b19cf407b7)**を見ていただく方がわかりやすい。

* [Redux の本家ページ](http://rackt.org/redux/index.html)
* [Redux入門 Reduxとは（公式ドキュメント和訳）](http://qiita.com/kiita312/items/b001839150ab04a6a427)
* [ReduxとES6でReact.jsのチュートリアルの写経](http://blog.bokuweb.me/entry/redux-tutorial)

## Redux 対応のための変更ポイント

#### Action の作成

Redux では、状態を変更する操作については、Actionと呼ばれる、JavaScriptのオブジェクトを作成して、それを Store に連携することで実施するのが原則となります。今回のTutorialでいうと、以下の二つがActionとなります。
* コメントを追加する。
* サーバーからのコメント一覧取得を反映する。

以下の addComment , setComments が Action と呼ばれるJavaScriptのオブジェクトを作成する関数となります。Actionオブジェクトは必ず type という属性を持つ必要があります。

```js
//src/actions/commentAction.js
import * as ActionTypes from '../constants/ActionTypes'

export function addComment(comment) {
  return {
    type: ActionTypes.ADD_COMMENT,
    comment
  };
}

export function setComments(comments) {
  return {
    type: ActionTypes.SET_COMMENTS,
    comments
  };
}
```

#### Reducer の作成

Reducer には Action を受けて、Store の State を変更するための手続きを記述することになります。
Actionオブジェクトには type属性が必ず存在するルールがあるので、それに従って分岐して記述していきます。

```js
//src/reducers/commentReducer.js
import * as ActionTypes from '../constants/ActionTypes'

export default function commentReducer(state={comments : []}, action) {
  switch(action.type){
    case ActionTypes.ADD_COMMENT:
      return {comments: state.comments.concat([action.comment])}
    case ActionTypes.SET_COMMENTS:
      return {comments: action.comments}
    default:
      return state;
  }
}
```

#### Store の作成と React と Redux の接続

Store 作成および React と Redux の接続に関しては、基本的には記述がある程度決まっているので Redux の情報に従って記述していきます。

* Store を作成するための関数です。Redux の logger を利用して、ChromeのDevTool に Redux 変更時にログが出力されるようにしています。

```js
//src/store/reduxStore.js
import {createStore,  applyMiddleware} from 'redux';
import commentReducer from '../reducers/commentReducer'
import createLogger from 'redux-logger';

export default function configureStore() {
  const logger = createLogger({logger:console});
  const createStoreWithMiddleware = applyMiddleware(
    logger
  )(createStore);
  const store = createStoreWithMiddleware(commentReducer);
  return store;
}
```

* Redux と React をつなげる部分です。connect 関数の第一引数に React に対して Storeの情報を propsを利用して渡すための変換ロジックを記述します。第二引数は、ReactからStoreを変更するための関数をActionを利用して作成しているロジックです。

```js
//src/containers/App.js
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import CommentBox from '../jsx/commentBox.jsx';
import * as commentActions from '../actions/commentAction';

function mapStateToProps(state) {
  return {
    url: '/api/comments',
    pollInterval: 2000,
    comments: state.comments
  };
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(commentActions, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(CommentBox)
```

* store を作成して、Reactのアプリ(```<App>```)を react-redux の Provider にくるむ感じで記述します。

```js
//src/jsx/example.jsx
import React  from 'react';
import ReactDOM  from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from '../store/reduxStore';
import App from '../containers/app';

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('content')
);

```

#### React の state を利用している部分を Redux を利用するように変更

* React のロジック中の stateを利用している部分および setState()を利用している部分を Redux を利用するように置き換えます。上記の```bindActionCreators(commentActions, dispatch)```のロジックにより commentActions の関数が React の props で利用できるようになっているのでそれを利用します。現在のStoreの値も ```mapStateToProps(state) {//}```で指定した形でprops 経由で利用可能となっています。

```diff
  //src/jsx/commentBox.jsx の diff 抜粋

   loadCommentsFromServer() {
@@ -19,36 +16,42 @@ export default class CommentBox extends React.Component{
       url: this.props.url,
       dataType: 'json',
       cache: false,
-      success: (data) => this.setState({data: data}),
+      success: (data) => {
+        this.props.setComments(data)
+      },
       error: (xhr, status, err) => console.error(this.props.url, status, err.toString())
     });
   }

   handleCommentSubmit(comment) {
-    let comments = this.state.data;
+    let comments = this.props.comments;
     comment.id = Date.now();

     let newComments = comments.concat([comment]);
-    this.setState({data: newComments});
+    this.props.setComments(newComments)
```

#### 対応完了版

以下のGitHubで公開しています。
https://github.com/ma-tu/react-babel-gulp-browserify-tutorial/tree/es6-redux

実行方法については 上記 GItHub の README.md で記述しています。

```
git clone https://github.com/ma-tu/react-babel-gulp-browserify-tutorial.git
cd react-babel-gulp-browserify-tutorial
git checkout es6-redux

npm install

gulp
```

## Mocha + power-assert を利用したテストについて

Mocha + powerについては参考情報は以下を参考にしました。感謝
[0からはじめるpower-assert](http://lealog.hateblo.jp/entry/2014/12/01/121031)

### Action , Store , Reducer に関するテスト

Action , Store , Reducer は UI からも分離されているので、素直に実行した結果のJavaScriptオブジェクトを比較する形でテストは記述可能です。

```js
//test/actions/commentActionTest.js
import assert from 'power-assert';
import * as commentActions from '../../src/actions/commentAction';
import * as ActionTypes from '../../src/constants/ActionTypes'
import * as commentUtil from '../testUtil/commentUtil'

describe('actions', () => {
    it('addComment', () => {
        const actual = commentActions.addComment(commentUtil.getPeteComment())
        const expected = {type: ActionTypes.ADD_COMMENT, comment: commentUtil.getPeteComment()}

        assert.deepEqual(actual, expected)
    });

    it('setComments', () => {
        const actual = commentActions.setComments([commentUtil.getPeteComment()])
        const expected = {type: ActionTypes.SET_COMMENTS, comments: [commentUtil.getPeteComment()]}

        assert.deepEqual(actual, expected)
    });

});
```

### React の UI を含んだテスト

jsdom と呼ばれるライブラリを利用してテストをします。jsdom を利用すると仮想的なDOM環境をテストコードで利用できるのでブラウザを利用しない形で、Reactのテストを行うことができます。

* 以下のスクリプトで　jsdom をテストで利用するための準備を行っています。この setup.js は mocha 実行時に呼び出されます。

```js
//test/setup.js
import { jsdom } from 'jsdom'

global.document = jsdom('<!doctype html><html><body></body></html>')
global.window = document.defaultView
global.navigator = global.window.navigator
```

以下が React の UI を含んだテスト例となります。

* ```setup()``` で React で用意されている TestUtils を利用してテスト用のMockを呼び出すようにしています。また初期データとして Paul さんのコメントデータを追加しています。
* ```TestUtils.findRenderedDOMComponentWithTag``` 関数でTagを利用してReactの仮想DOMを取得しています。
* ```TestUtils.scryRenderedDOMComponentsWithTag``` 上記と同じですが、scry…は取得結果が複数件である場合に利用します。
* ```container/App-display``` のテストでは初期表示状態で Paul さんが表示されていること。またはコメントにはMarkdownの装飾が入っていることを確認しています。
* ```container/append comment```  のテストでは TestUtils.Simulate の機能を利用して、コメント欄に入力を行って、submitを実行して、その結果が期待通りであるかを確認しています。

```js
//test/containers/AppTest.js
import assert from 'power-assert';
import React from 'react'
import ReactDOM  from 'react-dom';
import { Provider } from 'react-redux';
import TestUtils from 'react-addons-test-utils'
import configureStore from '../../src/store/reduxStore';
import MockApp from '../testUtil/MockApp';
import CommentList from '../../src/jsx/commentList.jsx';
import * as commentActions from '../../src/actions/commentAction';
import * as commentUtil from '../testUtil/commentUtil'


function setup() {
  const store = configureStore();
  store.dispatch(commentActions.addComment(commentUtil.getPaulComment()))

  const component = TestUtils.renderIntoDocument(
    <Provider store={store}>
      <MockApp />
    </Provider>
  )
  return {
    component: component,
    h2: TestUtils.findRenderedDOMComponentWithTag(component, 'h2'),
    span: TestUtils.findRenderedDOMComponentWithTag(component, 'span'),
    inputs: TestUtils.scryRenderedDOMComponentsWithTag(component, 'input')
  }
}

describe('container/App', () => {
  it('display', () => {
    const { h2, span } = setup()
    assert.equal(h2.innerHTML, "Paul O’Shannessy")
    assert.equal(span.innerHTML.trim(), "<p>React is <em>great</em>!</p>")
  })

  it('append comment', () => {
    const { component, inputs } = setup()
    TestUtils.Simulate.change(inputs[0], {target: {value: 'Test Author'}});
    TestUtils.Simulate.change(inputs[1], {target: {value: 'Test **Text**'}});
    TestUtils.Simulate.submit(inputs[2]);

    const h2List = TestUtils.scryRenderedDOMComponentsWithTag(component, 'h2')
    assert.deepEqual(h2List.map((item) => item.innerHTML), [ 'Paul O’Shannessy', 'Test Author' ])

    const spanList = TestUtils.scryRenderedDOMComponentsWithTag(component, 'span')
    assert.deepEqual(spanList.map((item) => item.innerHTML.trim()), [ '<p>React is <em>great</em>!</p>','<p>Test <strong>Text</strong></p>' ])
  })
})
```

### テスト実行

npm の package.json で mocha をコマンドで実行するようにしています。
```npm run test``` で実行できます。

```json
  //package.json
  "scripts": {
    "test": "mocha --compilers js:espower-babel/guess test/**/*.js --require ./test/setup.js"
  },
```