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

  • 109
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

前回 QiitaにReact.js の React.js の公式Tutorial を gulp を利用して簡単に実行できる環境を作って、ES6も試した
で公式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の基礎を見ていただく方がわかりやすい。

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

Action の作成

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

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

//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属性が必ず存在するルールがあるので、それに従って分岐して記述していきます。

//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 変更時にログが出力されるようにしています。
//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を利用して作成しているロジックです。
//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 にくるむ感じで記述します。
//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 経由で利用可能となっています。
  //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

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

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

//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 実行時に呼び出されます。
//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を実行して、その結果が期待通りであるかを確認しています。
//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 で実行できます。

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