React+ReduxのAction、Reducerを個別にテストする



##**概要**

Reactでユニットテストをしたく、コンポーネントのテストであれば以下の様な記事を参考にすればできそうだが、Fluxをつかったアプリで、ActionやDispatcherにロジックがある時には、それでは十分で無いと考えた。

* <a href="http://qiita.com/koba04/items/2f97904b3ca247fc1917" target="_blank">React.js + Jest - Qiita</a>

今回はReact+Reduxを使っているので、Action、Reducerを個別にテストする方針を考えた。テストフレームワークは、karma+jasmineを使用している。

##**Action**

テストするactionは以下の様なものを想定している。単純にAPIを叩いてresを得るというもの。

```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() {
  // redux-thunkを使用している
  return (dispatch, getState) => {
    dispatch(request());
    request
      .get('http://apiserver.com/someuri')
      .end(function(err, res) {
        return dispatch(requestSuccess(res.body.items));
      });
  };
}
```

このactionのテストは、以下のように実装した。reducerをモックして、actionの動作のみをテストするようにしている。

```action.spec.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';

import {
  REQUEST,
  REQUEST_SUCCESS,
} from 'constants';
import { createReducer } from 'utils';
import { fetchSomeResource } from './action.js';

describe('Actionのテスト', function() {
  let actionType = '';
  const middleware = [thunk];

  const initialState = {
    items: [],
    isFetching: false,
  };

  const mockStore = {
    SomeState: initialState,
  };

  // actionのテストのみ実装したく、actionの役割は、dispatchするまでなので、reducerはモックする
  // createReducerは以下の様なutilメソッドを用意しておく
  // export function createReducer (initialState, reducerMap) {
  //   return (state = initialState, action) => {
  //     const reducer = reducerMap[action.type];
  //     return reducer ? reducer(state, action.payload) : state;
  //   };
  // }
  const SomeState = createReducer(initialState, {
    [REQUEST]: (state) => {
      actionType = 'REQUEST';
      return state;
    },
    [REQUEST_SUCCESS]: (state) => {
      actionType = 'REQUEST_SUCCESS';
      return state;
    },
  });

  const rootReducer = combineReducers({
    SomeState,
  });

  const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);

  // actionのテストなので、ビューを作る必要はなく、storeまででよい
  const store = createStoreWithMiddleware(rootReducer, mockStore);

  it('REQUEST, REQUEST_SUCCESS が disapatch されること', (done) => {
    store.dispatch(fetchSomeResource());

    // 正常にactionが動作していれば、モックしたreducerの中で、actionTypeが更新される
    expect(actionType).toBe('REQUEST');

    setTimeout(function () {

      // 正常にactionが動作していれば、モックしたreducerの中で、actionTypeが更新される
      expect(actionType).toBe('REQUEST_SUCCESS');

      done();
    }, 2000);
  });
});
```


##**Reducer**

以下の様なシンプルなreducerを想定。

```js:reducer.js

// createReducerは以下の様なutilメソッドを用意しておく
// export function createReducer (initialState, reducerMap) {
//   return (state = initialState, action) => {
//     const reducer = reducerMap[action.type];
//     return reducer ? reducer(state, action.payload) : state;
//   };
// }
import { createReducer } from 'utils';

import {
  REQUEST,
  REQUEST_SUCCESS,
} from 'constants';

const initialState = {
  items: [],
  isFetching: false,
};

export default createReducer(initialState, {
  [REQUEST]: (state) => {
    return {
      ...state,
      isFetching: true,
    };
  },
  [REQUEST_SUCCESS]: (state, action) => {
    return {
      ...state,
      items: action.items,
      isFetching: false,
    };
  },
});
```

テストは以下のようになる。actionのテストとは反対に、actionからdispatchする必要が無く、reducerのロジックのみに集中できる。

```js:reducer.spec.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';

import {
  REQUEST,
  REQUEST_SUCCESS,
} from 'constants';
import SomeReducer from './reducer.js';

describe('Reducerのテスト', function() {
  const middleware = [thunk];

  const initialState = {
    items: [],
    isFetching: false,
  };

  const mockStore = {
    SomeState: initialState,
  };

  const rootReducer = combineReducers({
    SomeState: SomeReducer,
  });

  const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);
  const store = createStoreWithMiddleware(rootReducer, mockStore);

  it('REQUEST', (done) => {
    // reducerのテストなので、actionからdispatchする必要が無い。
    store.dispatch({ type: REQUEST });

    setTimeout(function () {
      // stateをチェックするにはgetState()を使う
      const { SomeState } = store.getState();
      expect(SomeState.isFetching).toBe(true);
      done();
    }, 1000);
  });

  it('REQUEST_SUCCESS', (done) => {
    const items = [
      { 'id': 1,
        'name': 'aaa',
      },
      { 'id': 2,
        'name': 'bbb',
      },
    ];

    store.dispatch({
      type: REQUEST_SUCCESS,
      items: items,
    });

    setTimeout(function () {
      const { SomeState } = store.getState();
      expect(SomeState.isFetching).toBe(false);
      expect(SomeState.items).toBe(items);
      done();
    }, 1000);
  });
});
```

##**追記**

※方針を変えました

* <a href="http://qiita.com/okmttdhr/items/decedca042e161980220" target="_blank">React+Reduxでasyncなactionをテストする - Qiita</a>
* <a href="http://qiita.com/okmttdhr/items/d5fcea3a54b4ad15e145" target="_blank">React+Reduxでreducerのテスト - Qiita</a>

##**参考**

<a href="http://kentor.me/posts/testing-react-and-flux-applications-with-karma-and-webpack/" target="_blank">Testing React and Flux applications with Karma and Webpack</a>