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

## はじめに
### redux-saga-test-planとは
[redux-saga-test-plan](https://github.com/jfairbank/redux-saga-test-plan)は[redux-saga](https://github.com/redux-saga/redux-saga/blob/master/README_ja.md)を使って書かれたコードのテストを行うためのライブラリです。

redux-saga（以下、Saga）って何？という方、あるいは、もう少し復習したい方は、Qiitaならば『[redux-sagaで非同期処理と戦う](https://qiita.com/kuy/items/716affc808ebb3e1e8ac)』が分かりやすくておススメなので、ご一読ください。

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

## 環境構築
### 実行環境
- Mac OS Sierra: v10.12.6
- node: v8.2.0
- npm: v5.3.0
- yarn: v1.2.1

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

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

### redux-sagaのインストール
続けて、Sagaをインストールします。

```sh
yarn add redux-saga
```

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

```sh
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](https://survivejs.com/blog/redux-saga-test-plan-interview/)（redux-saga-test-plan作者のインタビュー記事）および[redux-saga-test-plan公式](http://redux-saga-test-plan.jeremyfairbank.com/)より引用しています。

### パラメータ無しの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_SUCCESS`actionを伴った`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_USER`actionのオブジェクトを実引数として`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_USER`actionを`put`し、expectSagaで期待（assert）した`put`の内容と一致し、テストが通ることになります。

### モックデータを用いたテスト
前述の２つのテストでは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とモックデータの組み合わせ（配列）を要素として格納した１つの配列を引数に取ります。

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_REQUEST`actionをdispatchします。

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

```
[WARNING]: Saga exceeded async timeout of 250ms
```

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

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

https://github.com/galoi/redux-saga-test-plan-sample