Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

概要

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

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

方針

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

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

テストは以下のようになった。基本的にはこの記事と同じように実装している。

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());
  });
});
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の役割をしてくれるよう。

okmttdhr
dmmcom
総合エンタテイメントサイト「DMM.com」を運営。会員数は2,900万人を突破。動画配信、FX、英会話、ゲーム、太陽光発電、3Dプリンタなど40以上のサービスを展開。沖縄での水族館事業参入、ベルギーでのサッカークラブ経営など、様々な事業を手掛ける。また2018年より若手起業家の支援を強化、「DMM VENTURES」による出資や、M&Aなどを積極的に展開している。
https://dmm-corp.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away