Reactでアプリケーションを開発する際には専ら、Reduxを使うことが多いと思います。
React/Reduxのアプリケーションのユニットテストを書くとき、action,reducerのテストは比較的簡単ですが、middlewareが結構面倒くさいなと感じました。
middlewareはロジックが集約する場所であり、storeの更新や更なるactionのdispatchなどアプリケーションのコアとなる部分です。
そのため、テストを行う重要度もそれに比例して高いと思われます。
その割りには、middlewareのテストの書き方を紹介してる記事が少なかったので、ここに書き記しておきます。
カウンター
「+ボタン」が押されたらカウント値が +1 「-ボタン」が押されたらカウント値が -1 されるような簡単なカウンターを例にして説明します。
また、カウンターには以下の条件があります。
- カウント値が0になったら「-ボタン」にdisabledをかける
- カウント値が1以上かつ「-ボタン」にdisabledがかかっている場合、解除する
- 「-ボタン」の使用可否を制御するactionは上記2条件のタイミングにのみ発火する
この3つの条件をmiddlewareに記述します。
actionとreducer
「+ボタン」が押されたらカウント値が +1 「-ボタン」が押されたらカウント値が -1 されるような簡単なカウンターを想定してactionとreducerを作成します。
// src/actions/counter.js
export const incriment = () => ({
type: 'INCRIMENT',
});
export const decriment = () => ({
type: 'DECRIMENT',
});
export const disabled = payload => ({
type: 'DISABLED',
payload,
});
// src/reducers/counter.js
const INITIAL_STATE = {
count: 0,
disabled: true,
};
export const counter = (state = INITIAL_STATE, action) => {
const { type, payload } = action;
switch (type) {
case 'INCRIMENT': {
return {
...state,
count: state.count + 1,
};
}
case 'DECRIMENT': {
return {
...state,
count: state.count - 1,
};
}
case 'DISABLED': {
return {
...state,
disabled: payload,
};
}
default:
return state;
}
};
middleware
// src/middlewares/counter.js
import { disabled } from '../actions/counter';
export const countInspector = store => next => action => {
const { type } = action;
switch (type) {
case 'INCRIMENT': {
const { disabled } = store.getState().counter;
if (disabled) store.dispatch(disabled(false));
break;
}
case 'DECRIMENT': {
const { count, disabled } = store.getState().counter;
if (count === 1) store.dispatch(disabled(true));
else if (disabled) store.dispatch(disabled(false));
break;
}
default:
}
next(action);
};
mockの作成
middlewareのテストを行うためには、middlewareを実行しなければいけません。
最初にテストを書こうとすると、先ずこの実行させるところで躓きます。
更に、検証するものをどうやって返り値(actual)として取得するのかを考えないといけません。
middlewareで確認すべき項目は、以下の4項目です。
- dispatchされたactionの数
- nextされたactionの数
- actionの引数(payload)
- dispatch, nextされた順番
middlewareの実行のさせ方ですが
countInspector = store => next => action =>
このあまり見慣れない関数で戸惑うと思います。
関数のカリー化や部分適用をやろうとするとする時にこの書き方がでてきます。
ただ単純に実行するだけならばcountInspector()()();
で実行できますが、これでは動かないです。
なので、middlewareを実行できるように、なおかつ上記の4項目を確認できるようにするためのmockを作成します。
const storeHelpers = (fakeStore= {}, action, middleware) => {
const dispatched = [];
const nexted = [];
const fakeAction = typeof action === 'function' ? action() : action;
const store = {
getState() { return fakeStore; },
dispatch(dispatchAction) { dispatched.push(dispatchAction); },
};
const next = nextAction => nexted.push(nextAction);
middleware(store)(next)(fakeAction);
return {
dispatched,
nexted,
};
}
export default storeHelpers;
3つの引数はそれぞれ
- fakeStore : storeのmock
- action : middlewareがキャッチするaction
- middleware : テスト対象のmiddleware
を意味しております。
dispatch, nextされたactionをそれぞれdispatched, nextedという配列にpushして溜め、それをリターンするようにしています。
テストの作成
middlewareのテストのみここでは作成します。
テスト環境はmochaを使い、アサーションライブラリにpower-assertを使います。
// test/middlewares/counter.spec.js
import assert from 'power-assert';
import * as actions from '../../src/actions/counter';
import { countInspector } from '../../src/middlewares/counter';
import storeHelpers from './sotre-helpers';
describe('middlewares/counter', () => {
describe('countInspector', () => {
describe('無関係のactionを受け取った時', () => {
const fakeAction = { type: 'fakeAction' };
it('nextされるのみか', () => {
const {
dispatched,
nexted,
} = storeHelpers(null, fakeAction, countInspector);
assert.deepEqual(dispatched, []);
assert.deepEqual(nexted, [fakeAction]);
});
});
describe('action: INCRIMENT を受け取った時', () => {
const createFakeStore = value => ({
counter: { disabled: value },
});
describe('disabled : false である時', () => {
it('nextされるのみか', () => {
const fakeStore = createFakeStore(false);
const {
dispatched,
nexted,
} = storeHelpers(fakeStore, actions.incriment, countInspector);
const expected = [{ type: 'INCRIMENT' }];
assert.deepEqual(dispatched, []);
assert.deepEqual(nexted, expected);
});
});
describe('disabled : true である時', () => {
it('disabled(false)がdispatchされるか', () => {
const fakeStore = createFakeStore(true);
const {
dispatched,
} = storeHelpers(fakeStore, actions.incriment, countInspector);
const expected = [{ type: 'DISABLED', payload: false }];
assert.deepEqual(dispatched, expected);
});
it('incrimentがnextされるか', () => {
const fakeStore = createFakeStore(true);
const {
nexted,
} = storeHelpers(fakeStore, actions.incriment, countInspector);
const expected = [{ type: 'INCRIMENT' }];
assert.deepEqual(nexted, expected);
});
});
});
describe('action: DECRIMENT を受け取った時', () => {
const createFakeStore = (count, value = false) => ({
counter: {
count,
disabled: value,
},
});
describe('count値が2以上である場合', () => {
it('nextされるのみか', () => {
const fakeStore = createFakeStore(2, false);
const {
dispatched,
nexted,
} = storeHelpers(fakeStore, actions.decriment, countInspector);
const expected = [{ type: 'DECRIMENT' }];
assert.deepEqual(dispatched, []);
assert.deepEqual(nexted, expected);
});
});
describe('count値が2以上であり、disabled : true である場合', () => {
const fakeStore = createFakeStore(2, true);
it('disabled(false)がdisaptchされるか', () => {
const {
dispatched,
} = storeHelpers(fakeStore, actions.decriment, countInspector);
const expected = [{ type: 'DISABLED', payload: false }];
assert.deepEqual(dispatched, expected);
});
it('decrimentがnextされるか', () => {
const {
nexted,
} = storeHelpers(fakeStore, actions.decriment, countInspector);
const expected = [{ type: 'DECRIMENT' }];
assert.deepEqual(nexted, expected);
});
});
describe('count値が1である場合', () => {
const fakeStore = createFakeStore(1);
it('disabled(true)がdisaptchされるか', () => {
const {
dispatched,
nexted,
} = storeHelpers(fakeStore, actions.decriment, countInspector);
const dispatchExpected = [{ type: 'DISABLED', payload: true }];
const nextExpected = [{ type: 'DECRIMENT' }];
assert.deepEqual(dispatched, dispatchExpected);
assert.deepEqual(nexted, nextExpected);
});
});
});
});
});
長いですが、countInspecterの条件分岐を完全網羅するテストを作成する場合、このようなコードになります。
const {
dispatched,
nexted,
} = storeHelpers(fakeStore, actions.incriment, countInspector);
storeHelpersを使う時は、このようにdispatchedとnextedを受け取るようにし、
元コードに記述してあるactionとmiddlewareを渡すようにします。
素のmiddlewareを使う場面はそんなに多くはないですが、middlewareのテストを行う時は参考にしてみてください。