jest で Redux をテストする

[jestで非同期関数をテストする - Qiita](https://qiita.com/sand/items/3394f8a1244bd08fca3f)
[jest で React component をテストする - Qiita](https://qiita.com/sand/items/af2af0766ca00558457d)
[jest で Redux をテストする - Qiita](https://qiita.com/sand/items/72e816ef9deb1b425cae)

---



以下のドキュメントに沿って、jestでReduxをテストする手順を追います。
[Writing Tests - Redux](https://redux.js.org/recipes/writing-tests)


# １．環境設定

テストディレクトリを作る。

```
mkdir redux
cd redux/
```

reduxをテストするための必要最小限のパッケージをインストールする。

```
yarn add redux
yarn add --dev jest babel-jest @babel/preset-env 
```

babelの設定ファイルです。

```javascript:babel.config.js
module.exports = {
  presets: ['@babel/preset-env'],
};
```

package.jsonに以下を追加します。

```json:package.json
{
  "scripts": {
    "test": "jest"
  }
}
```


#２．Action Creators

Action CreatorはAction オブジェクトを作成してくれるものです。テストでは、Action Creatorを正しく呼びだすことができること、retrunされたAction オブジェクトが期待するものであるかをテストします。


```javascript:ActionTypes.js
export const ADD_TODO = 'ADD_TODO'
```

以下がテスト対象となるAction Creatorです。

```javascript:TodoActions.js
import * as types from './ActionTypes'

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
```

以下がAction creatorをテストするものです。

```javascript:TodoActions.test.js
import * as actions from './TodoActions'
import * as types from './ActionTypes'

describe('actions', () => {
  it('should create an action to add a todo', () => {
    const text = 'Finish docs'
    const expectedAction = {
      type: types.ADD_TODO,
      text
    }
    expect(actions.addTodo(text)).toEqual(expectedAction)
  })
})
```

実行結果は以下のようになります。

```
$ yarn test
yarn run v1.16.0
warning package.json: No license field
$ jest
 PASS  ./TodoActions.test.js
  actions
    ? should create an action to add a todo (14ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.255s
Ran all test suites.
Done in 4.50s.
```



#３． Async Action Creators

Async Action Creatorsのテストでは、Async Action関数の中で、期待通りのActionが実行されていることをテストします。

[redux-thunk](https://github.com/reduxjs/redux-thunk) を使うことにより、Actionとして関数（Async Action Creator）を使うことができるようになります。

ここでは[dmitry-zaets/redux-mock-store](https://github.com/dmitry-zaets/redux-mock-store) を使って、Async Action Creatorをテストします。


```
yarn add cross-fetch redux-thunk
yarn add --dev fetch-mock redux-mock-store
```


以下がテストの対象となるAsync Action Creatorです。

```javascript:ActionTypes.js
export const FETCH_TODOS_REQUEST = 'FETCH_TODOS_REQUEST'
export const FETCH_TODOS_SUCCESS = 'FETCH_TODOS_SUCCESS'
export const FETCH_TODOS_FAILURE = 'FETCH_TODOS_FAILURE'
```

```javascript:TodoActions.js
import 'cross-fetch/polyfill'
import * as types from './ActionTypes'

function fetchTodosRequest() {
  return {
    type: types.FETCH_TODOS_REQUEST
  }
}

function fetchTodosSuccess(body) {
  return {
    type: types.FETCH_TODOS_SUCCESS,
    body
  }
}

function fetchTodosFailure(ex) {
  return {
    type: types.FETCH_TODOS_FAILURE,
    ex
  }
}

export function fetchTodos() {
  return dispatch => {
    dispatch(fetchTodosRequest())
    // Return the promise
    return fetch('http://example.com/todos')
      .then(res => res.json())
      .then(body => dispatch(fetchTodosSuccess(body)))
      .catch(ex => dispatch(fetchTodosFailure(ex)))
  }
}
```

Async Action CreatorのfetchTodos()は関数を返しますが、その関数はpromiseを返していることに注意してください。

 **cross-fetch** は Node や Browsers、 React NativeのためのFetch APIです。==> [cross-fetch](https://www.npmjs.com/package/cross-fetch)



以下のテストで、Async Action の中で正しくActionが実行されていることを確認します。


```javascript:TodoActions.test.js
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import * as actions from './TodoActions'
import * as types from './ActionTypes'
import fetchMock from 'fetch-mock'


const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)

describe('async actions', () => {
  afterEach(() => {
    fetchMock.restore() // fetchMockのリセット
  })

  it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', () => {
    // fetchのmock化、一度だけ。
    fetchMock.getOnce('http://example.com/todos', {
      body: { todos: ['do something'] },
      headers: { 'content-type': 'application/json' }
    })

    // Async Action の中で実行されると期待されるAction配列
    const expectedActions = [
      { type: types.FETCH_TODOS_REQUEST },
      { type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } }
    ]

    const store = mockStore({ todos: [] })

    // Return the promise
    return store.dispatch(actions.fetchTodos()).then(() => {
      console.log(JSON.stringify(store.getActions())) // 結果出力
      expect(store.getActions()).toEqual(expectedActions)
    })
  })
})
```


**fetch-mock** によって、fetch を使ったhttp requestをmock化することができます。 ==> [fetch-mock - doc](http://www.wheresrhys.co.uk/fetch-mock/)


ここで使われている**redux-mock-store API**の説明は以下の通りです。

* **configureStore(middlewares?: Array) => mockStore: Function** : middlewaresを指定してmock storeを作ります。
* **mockStore(getState?: Object,Function) => store: Function** : mock storeのインスタンスを返します。storeのリセットにも使われます。
* **store.dispatch(action) => action** : mock storeに対して action をdispatch します。 actionはインスタンス(store)に保存され、実行されます。
* **store.getActions() => actions: Array** : インスタンス(store)に保存されたactionの配列を返します。


実行結果として、インスタンスに保存されたaction配列を出力したものです。

```
[{"type":"FETCH_TODOS_REQUEST"},{"type":"FETCH_TODOS_SUCCESS","body":{"todos":["do something"]}}]
```

テストコマンドの出力結果です。

```
$ yarn test
yarn run v1.16.0
warning package.json: No license field
$ jest
 PASS  ./TodoActions.test.js
  async actions
    ? creates FETCH_TODOS_SUCCESS when fetching todos has been done (51ms)

  console.log TodoActions.test.js:32
    [{"type":"FETCH_TODOS_REQUEST"},{"type":"FETCH_TODOS_SUCCESS","body":{"todos":["do something"]}}]

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.216s
Ran all test suites.
Done in 4.88s.
```


# ４．Reducers

Reducersのテストでは、stateの初期状態や、actionを実行した結果のstateが期待したものであることをテストします。

```javascript:ActionTypes.js
export const ADD_TODO = 'ADD_TODO'
```

以下がテスト対象となるReducerです。

```javascript:todos.js
import { ADD_TODO } from './ActionTypes'

const initialState = [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]

export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        },
        ...state
      ]

    default:
      return state
  }
}
```

ちょっと読みにくい部分があるとすれば、reduce関数でしょうか。idを設定するときに、現在stateにあるidの最大値を求めるのにreduceを使っています。意味的には、id=idの最大値+1、としています。 ==> [Array.prototype.reduce()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)

ちなみに個人的には、Huttonの「プログラミング Haskell」における以下の定義が記憶しやすいです。vが初期値、(+)が二項演算（関数）で、リストの左から順番に累積値を求めるさまが直感的に把握できます。

```
foldl (+) v [x0,x1,...,xn] = (...( (v (+) x0) (+) xn )...) (+) xn
```

以下がテスト関数です。まず初期状態をテストします。次にActionを加える前後の状態をテストします。

```javascript:todos.test.js
import reducer from './todos'
import * as types from './ActionTypes'

describe('todos reducer', () => {
  it('should return the initial state', () => {
    expect(reducer(undefined, {})).toEqual([
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })

  it('should handle ADD_TODO', () => {
    expect(
      reducer([], {
        type: types.ADD_TODO,
        text: 'Run the tests'
      })
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 0
      }
    ])

    expect(
      reducer(
        [
          {
            text: 'Use Redux',
            completed: false,
            id: 0
          }
        ],
        {
          type: types.ADD_TODO,
          text: 'Run the tests'
        }
      )
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 1
      },
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })
})
```

テストの実行結果です。passを確認します。

```
$ yarn test
yarn run v1.16.0
warning package.json: No license field
$ jest
 PASS  ./todos.test.js
  todos reducer
    ? should return the initial state (10ms)
    ? should handle ADD_TODO (2ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.539s
Ran all test suites.
Done in 3.97s.
```



※ちなみに、このドキュメントにあるEnzymeを使ったReact componentのテストについては、以下の記事にまとめてあります。
[jest で React component をテストする - Qiita](https://qiita.com/sand/items/af2af0766ca00558457d)


今回は以上です。



# 参考記事
[Testing in React with Jest and Enzyme: An Introduction](https://medium.com/@rossbulat/testing-in-react-with-jest-and-enzyme-an-introduction-99ce047dfcf8)