昨日はUnitテストの設定方法からComponentのユニットテストまで説明しました。今日は、ロジックのテストを解説します。React Nativeでアプリを作る場合は、Fluxアーキテクチャを選ぶと思います。Reduxはその中でも秀逸であり、Testの方法もしっかりドキュメントされています。
アプリのロジックをReduxを用いてテストしていきます。実はここはReact Nativeに限った話ではないかもしれません。しかし、Objective-Cやswiftでしっかりロジックのテストを書くのは難しい(私のswiftスキルがないのが原因だと思いますが。。)ように感じます。個人的にはアプリに状態を持たせて、それをテストできるというReduxの真骨頂がここにあるような気がしています。
Testing store with dispatch
Reduxのドキュメントではactionとreducerを別々にユニットテストしています。同期のactionとreducerはユニットテストできるのですが、非同期のactionのテストが少し難しいです。(例えば、nockがReact Nativeだと動かないなどでハマりました)
そこで私は、Reduxのissue#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のバージョン1系です。これだと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でも良いと思います)