そーす

福岡在住。iOS/Androidアプリ, Webフロントエンドのエンジニアです。Swift, Kotlin, JavaScript, ReactNative

ReactNative+Redux環境で非同期アクションのテストを書く

Javascriptでちゃんとテスト書いてますか?

私は書いてないです。

テストはあんまり書いたことないです(ドン引き

Javascriptのテストは全く書いたことないです(ドンッ!!

今仕事で開発しているアプリはReactNativeなんですが、

やっぱスクリプト言語だと実行時までミスがわからないので怖いですね…。

もう怯えながらリリースするのは嫌なんだ…

というわけで、テストを導入してます。

ちなみにESLintflowは導入してます。

Jest

テストライブラリはJestを採用しました。

facebook.github.io

理由は、

  • ReactNativeは最初からJestの環境が整っている。
  • 構文がシンプルで簡単(テスト初心者でも使えそう。
  • Fluxの実装にReduxを使っているが、Jestでのテスト方法が公式にある。

という感じです。

プロジェクトを作る

とりあえず、新規RNプロジェクトを作って導入までをまとめます。

> react-native init redux_jest_sample

最初からJestがpackage.jsonに追加されていて、すぐにjestが動作する環境が整っています。 デフォルトで_test_にテストが書いてあるので実行してみます。

> npm test

> redux_jest_sample@0.0.1 test /Users/Ryohlan/Dev/react-native/redux_jest_sample
> jest

 PASS  __tests__/index.android.js
 PASS  __tests__/index.ios.js

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.65s, estimated 10s
Ran all test suites.

テストがパスしました。

jestのテストは_test_ディレクトリ内か.text.js, .spec.jsが対象です。

通信のモック

nockというライブラリを使います。 github.com

nockは特定のリクエストが走ったときにその処理を横取りして任意のレスポンスを返してくれるライブラリです。

例えば、ReduxのHttp通信を行う非同期アクションのテストを行いたいときに、 通信のテストでは無いので特定の値を返してしまおうとういうものです。

 nock('http://hoge.com/api/v1')
        .get('/messages')
        .reply(200, { messages: ['hoge', 'fuga'] });

上記の例ではhttp://hoge.com/api/v1/messagesに対してGETメソッドでアクセスしたときに ステータスコード200で、レスポンスBodyを{ messages: ['hoge', 'fuga'] }で返すという書き方です。

ReduxのStoreのモック

redux-mock-storeというライブラリを使います github.com

非同期アクションが始まって終わるまでのアクションを保持してくれます。

非同期アクションのテスト

あとはアクションのテストを書くのみです。

//action
const REQUEST_MESSAGES = 'REQUEST_MESSAGES';
const SUCCESS_MESSAGES = 'SUCCESS_MESSAGES';
const requestMessges = () => ({ type: REQUEST_MESSAGES});
const successMessges = (messages) => ({ type: SUCCESS_MESSAGES , messages });
const fetchMessages = () => (dispatch) => {
 dispatch(requestMessges());
    return fetch('http://hoge.com/api/v1/messages')
               .then(response => response.json().then((json) => successMessages(json.message));
};


//reducer
const messages = (state = { messages: [], isFetching: false }, action )  => {
    switch(action.type) {
        case REQUEST_MESSAGES: 
            return Object.assign({}, state, { isFetching: true });
        case SUCCESS_MESSAGES: 
            return Object.assign({}, state, { messages: action.messages, isFetching: false };
        default: return state;
    }
};

//test
test('非同期テスト', () => {
    // Http通信のモック
  nock('http://hoge.com/api/v1')
        .get('/messages')
        .reply(200, { messages: ['hoge', 'fuga'] });

     const store = mockStore({}); // Reduxストアの初期化

      // ストアにアクションをディスパッチ
      return store.dispatch(Actions.fetchMessages())
        .then(() => expect(store.getActions()).toEqual(
          [
            { type: Actions.REQUEST_MESSAGES },
            { type: Actions.SUCCESS_MESSAGES, messages: ['hoge', 'fuga'] },
          ],
        ));
    });

重要なのは最後の

      return store.dispatch(Actions.fetchMessages())
        .then(() => expect(store.getActions()).toEqual(
          [
            { type: Actions.REQUEST_MESSAGES },
            { type: Actions.SUCCESS_MESSAGES, messages: ['hoge', 'fuga'] },
          ],
        ));
    });

です。

expect(A).toEqual(B)はAがBと同じであると言うJestのテスト構文です。

Actions.fetchMessages()でReduxの非同期アクションが始まります。

store.getActions()で非同期アクションの始まりから終わりまでのアクションが取得できるので、

期待されるアクションと比較することで非同期アクションのテストができるということです。