Reduxのmiddlewareのテストを書く

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を作成します。

```js
// src/actions/counter.js

export const incriment = () => ({
    type: 'INCRIMENT',
});

export const decriment = () => ({
    type: 'DECRIMENT',
});

export const disabled = payload => ({
    type: 'DISABLED',
    payload,
});
```

```js
// 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

```js
// 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のテストを行うためには、ｍiddlewareを実行しなければいけません。

最初にテストを書こうとすると、先ずこの実行させるところで躓きます。
更に、検証するものをどうやって返り値(actual)として取得するのかを考えないといけません。

middlewareで確認すべき項目は、以下の4項目です。

- dispatchされたactionの数
- nextされたactionの数
- actionの引数(payload)
- dispatch, nextされた順番

middlewareの実行のさせ方ですが
`countInspector = store => next => action =>`
このあまり見慣れない関数で戸惑うと思います。
関数のカリー化や部分適用をやろうとするとする時にこの書き方がでてきます。
ただ単純に実行するだけならば`countInspector()()();`で実行できますが、これでは動かないです。

なので、middlewareを実行できるように、なおかつ上記の4項目を確認できるようにするためのmockを作成します。

```js
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を使います。

```js
// 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の条件分岐を完全網羅するテストを作成する場合、このようなコードになります。

```js

const {
    dispatched,
    nexted,
} = storeHelpers(fakeStore, actions.incriment, countInspector);
```

storeHelpersを使う時は、このようにdispatchedとnextedを受け取るようにし、
元コードに記述してあるactionとmiddlewareを渡すようにします。

素のmiddlewareを使う場面はそんなに多くはないですが、middlewareのテストを行う時は参考にしてみてください。