21
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-10-09

##概要

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

今回は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を想定。

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のロジックのみに集中できる。

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);
  });
});

##追記

※方針を変えました

##参考

Testing React and Flux applications with Karma and Webpack

21
20
0

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
  3. You can use dark theme
What you can do with signing up
21
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?