26
27

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.

redux-saga-test-planを使って、redux-sagaのテストを書く

Last updated at Posted at 2018-09-09

はじめに

redux-saga-test-planとは

redux-saga-test-planredux-sagaを使って書かれたコードのテストを行うためのライブラリです。

redux-saga(以下、Saga)って何?という方、あるいは、もう少し復習したい方は、Qiitaならば『redux-sagaで非同期処理と戦う』が分かりやすくておススメなので、ご一読ください。

サンプルコードをベースにして説明しますが、そのサンプルコードを実行するための環境構築から始めます。
(テストコードは、create-react-appで標準で付いてくるテスト用フレームワークのJestを使って書いていきます)

環境構築

実行環境

  • Mac OS Sierra: v10.12.6
  • node: v8.2.0
  • npm: v5.3.0
  • yarn: v1.2.1

Reactアプリの構築

create-react-appを用いてReact環境を作り、プロジェクトルートまで移動します。

cd ~/
create-react-app redux-saga-test-plan-sample
cd redux-saga-test-plan-sample

redux-sagaのインストール

続けて、Sagaをインストールします。

yarn add redux-saga

redux-saga-test-planのインストール

redux-saga-test-planは開発用テストツールなので、devDependenciesとしてインストールします。

yarn add -D redux-saga-test-plan

サンプルのテストコード

通常、アプリで使用する際は、Sagaで書かれたコード(Sagaタスク)と、テストコードはそれぞれ別ファイルに保存しますが、便宜上、同じファイル内に書きます。

テストコードはsrcディレクトリに保存し、yarn testでテストを実行できます。

ここからのコードは、redux-saga-test-plan - Test Redux Saga with an easy plan - Interview with Jeremy Fairbank(redux-saga-test-plan作者のインタビュー記事)およびredux-saga-test-plan公式より引用しています。

パラメータ無しのGETリクエストによるAPIをcallするSagaタスクのテスト

src/getUsers.test.js
import { call, put } from "redux-saga/effects";
import { expectSaga } from "redux-saga-test-plan";

function* fetchUsersSaga(api) {
  const users = yield call(api.getUsers);
  yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
}

it("fetches users", () => {
  const users = ["Jeremy", "Tucker"];

  const api = {
    getUsers: () => users
  };

  return expectSaga(fetchUsersSaga, api)
    // 最終的に期待する結果
    .put({ type: "FETCH_USERS_SUCCESS", payload: users })

    // Sagaタスクをforkし、テストを開始
    .run();
});

パラメータ無しでGETリクエストし、単純に二人のユーザー名が格納された配列を返すAPIを、itで始まるテストコードの中に定義しています。

redux-saga-test-planからimportされたexpectSaga関数に対して、実引数として、Sagaタスク(fetchUsersSaga)とそのタスクが受け取る引数(api)を渡します。

expectSagaは、runメソッドによってSagaタスクをforkし、Sagaで実行される作用(putなど)を期待するAPIを返します。
(上記の例では、expectSagaのputメソッドによって、SagaがFETCH_USERS_SUCCESSactionを伴ったput作用をyieldすることをassertしています)

パラメータ有りのGETリクエストによるAPIをcallするSagaタスクのテスト

src/fetchUser.test.js
import { call, put, take } from 'redux-saga/effects';
import { expectSaga } from 'redux-saga-test-plan';

function* userSaga(api) {
  const action = yield take('REQUEST_USER');
  const user = yield call(api.fetchUser, action.payload);

  yield put({ type: 'RECEIVE_USER', payload: user });
}

it('fetch user', () => {
  const api = {
    fetchUser: id => ({ id, name: 'Tucker' }),
  };

  expectSaga(userSaga, api)
    // 最終的に期待する結果
    .put({
      type: 'RECEIVE_USER',
      payload: { id: 42, name: 'Tucker' },
    })

    // userSagaタスクがtakeするactionをdispatch
    .dispatch({ type: 'REQUEST_USER', payload: 42 })
    .run();
});

同じく、GETリクエストによるAPIのテストですが、パラメータ無しのケースと比較すると、expectSagaは、payload: 42を含んだREQUEST_USERactionのオブジェクトを実引数としてdispatchメソッドを実行しています。

runメソッドによってSagaタスクが開始されると、dispatchされたactionは、Sagaのtake作用によって受け取られ、一旦はactionに格納し、次のcall作用の第2引数にaction.payloadで値が渡されます。それをapi.fetchUser関数の実引数として受け取り、オブジェクトを返して、userに格納します。
(api.fetchUserはテストコードの中で定義されており、id(=42)を受け取ると、{ id: 42, name: 'Tucker' }を返します)

さらに、Sagaはpayloadとして受け取った上記userを含むRECEIVE_USERactionをputし、expectSagaで期待(assert)したputの内容と一致し、テストが通ることになります。

モックデータを用いたテスト

前述の2つのテストではAPIをテストコードの中で定義しましたが、redux-saga-test-planのprovideメソッドを使って、既存のAPIの返却値として、テストコード内で指定したモックデータを返すようにしてテストを行うことができます。

src/fetchUsersWithMocks.test.js
import { call, put } from "redux-saga/effects";
import { expectSaga } from 'redux-saga-test-plan';

const api = {
  getUsers: () => '',
};

function* fetchUsersSaga() {
  const users = yield call(api.getUsers);
  yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
}

it("fetches users", () => {
  const users = ["Jeremy", "Tucker"];

  return expectSaga(fetchUsersSaga)
    .provide([[call(api.getUsers), users]])
    .put({ type: "FETCH_USERS_SUCCESS", payload: users })
    .run();
});

expectSagaのprovideメソッドはmatcherとモックデータの組み合わせ(配列)を要素として格納した1つの配列を引数に取ります。

redux-saga-test-planは、matcherで指定された作用に対して、任意のモックデータを返すことができます。
matcherのcall作用で呼び出される既存のAPIの中身が何であれ、テストで実行されるSagaタスクでは、必ず任意に指定したモックデータが返ります。

エラーハンドリングのテスト

Sagaタスクのエラーハンドリング部分についても同様にテストを行うことができます。
redux-saga-test-plan/providersから、任意のエラーを発生させるthrowError関数をimportします。

src/fetchUsersSagaErrorHandling.test.js
import { call, put } from "redux-saga/effects";
import { expectSaga } from "redux-saga-test-plan";
import { throwError } from "redux-saga-test-plan/providers";

const api = {
  getUsers: () => ""
};

function* fetchUsersSaga() {
  try {
    const users = yield call(api.getUsers);
    yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
  } catch (e) {
    yield put({ type: "FETCH_USERS_FAIL", payload: e });
  }
}

it("handles errors", () => {
  const error = new Error("Whoops");

  return expectSaga(fetchUsersSaga)
    .provide([[call(api.getUsers), throwError(error)]])
    .put({ type: "FETCH_USERS_FAIL", payload: error })
    .run();
});

前述のモックデータを用いたテスト同様に、expectSaga のprovideメソッドを使って、matcherのcall作用に対して、エラーオブジェクト(error)を伴ったエラーが発生するように仕向けます。

そうすることで、fetchUsersSagaタスク内でcall作用が発生した際、必ずエラーが発生し、catch内の処理が実行されます。

特定のaction発行を監視して、別のSagaタスクをforkするSagaのテスト

takeLatest作用によって、特定のactionを監視して、別のSagaタスクをforkする(その前までに発行されたactionによってforkされたSagaタスクが未完了の場合は、そのタスクをキャンセルし、最新で発行されたactionを優先させる)Sagaタスクのテストを行います。

src/watchFetchUserSaga.test.js
import { call, put, takeLatest } from "redux-saga/effects";
import { expectSaga } from "redux-saga-test-plan";

const api = {
  getUser: () => ""
};

function* fetchUserSaga(action) {
  const id = action.payload;
  const user = yield call(api.getUser, id);

  yield put({ type: "FETCH_USER_SUCCESS", payload: user });
}

function* watchFetchUserSaga() {
  yield takeLatest("FETCH_USER_REQUEST", fetchUserSaga);
}

it("fetches a user", () => {
  const id = 42;
  const user = { id, name: "Jeremy" };

  return expectSaga(watchFetchUserSaga)
    .provide([[call(api.getUser, id), user]])
    .put({ type: "FETCH_USER_SUCCESS", payload: user })
    .dispatch({ type: "FETCH_USER_REQUEST", payload: id })
    .silentRun();
});

expectSagaの引数にはwatchFetchUserSagaを割り当てていますが、watchFetchUserSagaからforkされるfetchUserSagaのテストも行います。

expectSagaのsilentRunによってテストが開始されると、watchFetchUserSagaに向けて、payload: 42を含んだFETCH_USER_REQUESTactionをdispatchします。

takeLatest作用はループで処理されるので、silentRunの代わりにrunメソッドでテストを実行すると、redux-saga-test-planは下記のようなワーニングを発生させて、Sagaタスクをタイムアウトさせてしまいます。

[WARNING]: Saga exceeded async timeout of 250ms

silentRunメソッドを使用すると、このような警告が発生することなく、タイムアウトされます。
デフォルトでは250msでタイムアウトさせますが、silentRunの引数にミリ秒単位の数字を指定することで、タイムアウトするタイミングを調整することができます。

サンプルコード完成品

以下のリポジトリに上記のサンプルコードが置いてあります。

26
27
1

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
26
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?