React+Reduxでasyncなactionをテストする


#**概要**

reduxでactionのテストをしたく、`reducerをモックしてactionをテストする`という方針を以下の記事に書いた。

* <a href="http://qiita.com/okmttdhr/items/180c25a4a1188d55f842" target="_blank">React+ReduxのAction、Reducerを個別にテストする - Qiita</a>

しかし、これだと、asyncな関数の終了タイミングを担保できていないので、別の方針でテストすることにした。

#**方針**

reduxのドキュメントを参考にする。

* <a href="http://rackt.github.io/redux/docs/recipes/WritingTests.html" target="_blank">Writing Tests | Redux</a>

asyncなactionは以下のとおりである。

```action.js
import {
  REQUEST,
  REQUEST_SUCCESS,
} from 'constants';

// リソース管理はsuperagent
import request from 'superagent';

function request() {
  return {
    type: REQUEST,
  };
}

function requestSuccess(items) {
  return {
    type: REQUEST_SUCCESS,
    items: items,
  };
}

export function fetchSomeResource() {
  return (dispatch, getState) => {
    dispatch(request());
    request
      .get('http://apiserver.com/someuri')
      .end(function(err, res) {
        return dispatch(requestSuccess(res.body.items));
      });
  };
}
```

テストは以下のようになった。基本的には<a href="http://rackt.github.io/redux/docs/recipes/WritingTests.html" target="_blank">この記事</a>と同じように実装している。

```js:action.spec.js
import {
  REQUEST,
  REQUEST_SUCCESS,
} from 'constants';
import { createStoreForActionTest } from 'utils/test';
import { fetchSomeResource } from './action.js';

describe('(Action)', function() {
  const initialState = {
    items: [],
    isFetching: false,
  };

  const expectedActions = [
    { type: REQUEST },
    { type: REQUEST_SUCCESS, items: ['items'] },
  ];

  it('REQUEST, REQUEST_SUCCESS が disapatch されること', (done) => {
    const store = createStoreForActionTest(initialState, expectedActions, done);
    store.dispatch(fetchSomeResource());
  });
});
```

```js:utils/test.js
import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

/**
 * action のテストに必要な store を作成する。
 * dispatch をモックし、内部で、
 * 正しく disapatch されているかのテストもあわせて行っている。
 *
 * @param {Object/Function} getState
 * @param {Array} expectedActions
 * @param {Function} onLastAction
 */
export function createStoreForActionTest(getState, expectedActions, onLastAction) {
  const middleware = [thunk];

  if (!Array.isArray(expectedActions)) {
    throw new Error('expectedActions should be an array of expected actions.');
  }
  if (typeof onLastAction !== 'undefined' && typeof onLastAction !== 'function') {
    throw new Error('onLastAction should either be undefined or function.');
  }

  function mockStoreWithoutMiddleware() {
    return {
      getState() {
        return typeof getState === 'function' ?
          getState() :
          getState;
      },

      dispatch(action) {
        const expectedAction = expectedActions.shift();

        // ここが実際のテストコード
        expect(action).toEqual(expectedAction);

        if (onLastAction && !expectedActions.length) {
          onLastAction();
        }
        return action;
      },
    };
  }

  const mockStoreWithMiddleware = applyMiddleware(...middleware)(mockStoreWithoutMiddleware);

  return mockStoreWithMiddleware();
}
```

このやり方だと、reducerすらモックせず、イベントがdispatchされることを内部で検知していて、そこで適当な値かどうかもテストしている。

これにて、asyncな関数であっても問題なくテストができる。


# 追記

最近見たら、以下の様なライブラリがあって、ここでいうcreateStoreForActionTestの役割をしてくれるよう。

* <a href="https://github.com/arnaudbenard/redux-mock-store" target="_blank">redux-mock-store</a>