React Native - Unit Test part2 (Redux test with Jest)

昨日はUnitテストの設定方法からComponentのユニットテストまで説明しました。今日は、ロジックのテストを解説します。React Nativeでアプリを作る場合は、Fluxアーキテクチャを選ぶと思います。Reduxはその中でも秀逸であり、Testの方法もしっかり[ドキュメント](http://rackt.org/redux/docs/recipes/WritingTests.html)されています。

アプリのロジックをReduxを用いてテストしていきます。実はここはReact Nativeに限った話ではないかもしれません。しかし、Objective-Cやswiftでしっかりロジックのテストを書くのは難しい（私のswiftスキルがないのが原因だと思いますが。。）ように感じます。個人的にはアプリに状態を持たせて、それをテストできるというReduxの真骨頂がここにあるような気がしています。


## Testing store with dispatch

Reduxのドキュメントではactionとreducerを別々にユニットテストしています。同期のactionとreducerはユニットテストできるのですが、非同期のactionのテストが少し難しいです。(例えば、nockがReact Nativeだと動かないなどでハマりました)

そこで私は、Reduxの[issue#546](https://github.com/rackt/redux/issues/546)で話されている方法を使ってactionとreducerをまとめてテストしています。厳密にはユニットテストではないかもしれませんが、Reduxの挙動をまとめてテストできるので現実的な解としてはアリだと思っています。

下記のactionとreducerをテストします。一般的なGETでJSONを取得する非同期Fetchです。

### action

```
export const FETCH_PLACES_REQUEST = 'FETCH_PLACES_REQUEST';
export const FETCH_PLACES_SUCCESS = 'FETCH_PLACES_SUCCESS';
export const FETCH_PLACES_FAILURE = 'FETCH_PLACES_FAILURE';

function fetchPlacesRequest() {
  return {
    type: FETCH_PLACES_REQUEST
  }
}

function fetchPlacesSuccess(data) {
  return {
    type: FETCH_PLACES_SUCCESS,
    data: data,
  }
}

function fetchPlacesFailure(error) {
  return {
    type: FETCH_PLACES_FAILURE,
    error: error
  }
}

export function fetchPlaces(lat, lng, text) {
  return dispatch => {
    dispatch(fetchPlacesRequest())
    Foursquare.getVenueByLocationAndName(lat, lng, text)
      .then(res => res.json())
      .then(json => dispatch(fetchPlacesSuccess(json.body.results)))
      .catch(error => dispatch(fetchPlacesFailure(error)))
  }
}
```

### reducer

```
import {
  FETCH_PLACES_REQUEST,
  FETCH_PLACES_SUCCESS,
  FETCH_PLACES_FAILURE
} from './actions';

function places(state = {
  isFetching: false,
  data: [],
  error: null,
}, action){

  switch (action.type) {
    case FETCH_PLACES_REQUEST:
      return Object.assign({}, state, {
        isFetching: true,
      });
    case FETCH_PLACES_SUCCESS:
      return Object.assign({}, state, {
        isFetching: false,
        data: action.data
      });
    case FETCH_PLACES_FAILURE:
      return Object.assign({}, state, {
        isFetching: false,
        error: action.error,
      });
    default:
      return state;
  }
}
```

## Test file

これらをテストするのに下記を用います。storeをlistenしてeventが発動する度にテストを回します。


```
jest.autoMockOff(); // I don't need to auto mock -- could be a bad knowhow but it works witout stress:)

jest.setMock('../app/utils/Foursquare.js', require('../__mocks__/Foursquare.js')); // issue#335 on facebook/jest

var actions = require('../app/actions');
var configureStore = require('../app/configureStore');
var store = configureStore();

describe('Dispatching places action', () => {
  it(' succeeded when ', (done) => {

    var count = 0;

    var unsubscribe = store.subscribe(function() {
      let state = store.getState();
      console.warn(state)
      if(count === 0 ){
        expect(state.places).toEqual({
          isFetching: true,
          data: [],
          error: null,
        });
      }else if(count === 1){
        expect(state.places).toEqual({
          isFetching: false,
          data: [1,2,3],
          error: null,
        });
      }
      count++;
      if(count > 1){
        done();
      }
   })
   store.dispatch(actions.fetchPlaces(1,2,'foo'));
  })
})
```

Jestが自動でMockするのがデフォルトの挙動です。しかし、import/exportで記述するとうまく動かなかったりと、ハマりどころがたくさんあるのが私がJest+React Nativeを触った感想です。テストの環境でハマって時間を費やすのはナンセンスなので`jest.autoMockOff();`してしまいます。（バッドノウハウだと思います）

`Foursquare`は下記のようにPromiseでmockしています。

```
module.exports = {
  getVenueByLocationAndName(lat, lng, name){
    return new Promise((resolve, reject) => {
      resolve({json: this._json });
    });
  },

  _json(){
    return {
      body: {
        results: [1,2,3]
      }
    }
  }
}
```


JestのデフォルトはJasmineのバージョン１系です。これだと`done()`がないので、0.8からJasmineバージョン2に対応したので、package.jsonに`testRunner`の行を追加します。

```
  "jest": {
    "scriptPreprocessor": "node_modules/react-native/jestSupport/scriptPreprocess.js",
    "setupEnvScriptFile": "node_modules/react-native/jestSupport/env.js",
    "testPathIgnorePatterns": [
      "/node_modules/",
      "packager/react-packager/src/Activity/"
    ],
    "testFileExtensions": [
      "js"
    ],
    "testRunner": "<rootDir>/node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js",　// 追加
    "unmockedModulePathPatterns": [
      "promise",
      "react",
      "fbjs",
      "source-map"
    ]
  },
```

## Summary

Jest自体に癖があるし、React Nativeでは動かないもモジュールもあって環境設定に戸惑います。Reduxのテストをまとめてやると効率的だと思うので、今回のような方法を試されてはいかがでしょう（もちろん、普通のReactでも良いと思います）