search
LoginSignup
106

More than 5 years have passed since last update.

posted at

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

前回 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"
  },

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
106