Vuexをモックしてシンプルに単体テストを行う。〜With Jest〜

# はじめに
肥大化していく複雑さに対抗しうるはテストの存在。
[Vuex](https://vuex.vuejs.org/ja/)にてテストを欲するはmutationsとactions。
テストフレームワークには手に馴染む[Jest](https://jestjs.io/ja/)。
いざ書かん。

```js:Failure.test.js
import store from '@pass/to/store'

describe('Test Vuex module', () => {
  /**
   * store内部では
   * state = {
   *  items: []
   * }
   * で初期化されています。
   */

  test('itemsでstate.itemsを初期化', () => {
    const items = [
      { id: 1, name: 'hoge' }
    ]
    store.commit('models/User/setItems', items)
    expect(store.state.items).toEqual(items) //pass
  })

  test('state.itemに要素を追加', () => {
    const item = { id: 1, name: 'hoge' }
    store.commit('models/User/addItem', item)
    expect(store.state.items.length).toBe(1) //failure, received 2
  })
})
```

そして敗れる我がテスト。


上記のテストでは１つ目のテストは成功するものの、２つ目のテストでは配列の要素数が期待どおりではなく失敗しています。
Vuex自体の動作としては望ましいですが、stateの状態をテストスイート全体で追いかけるのは不便で面倒です。
（[公式のチュートリアル](https://vuex.vuejs.org/ja/guide/testing.html)ではmutationやactionを個別でインポートし、stateをモックすることでテストを行っています。）

**テストはそれ自体、書きやすくあってほしいものです。**

そこで本記事ではVuexにまつわる諸々をモック化し、mutationやactionのテストが楽になるような方法を提案します。
本記事で使用したコードは[ここ](https://github.com/kyhei/jest-vuex)にあるので気になった方は見てみてください。

モック後のテストコードのイメージはこんな感じです。

```js:mockedVuex.test.js
describe('Test your Vuex module', () => {

  test('mutationでstateを初期化', () => {
    const items = [
      { id: 1, name: 'hoge' }
    ]
    const store = new MockedVuexStore(state, mutations, actions)
    store.commit('setItems', items)
    expect(store.state.items).toEqual(items)
  })

  test('非同期なactionで要素を追加', () => {
    const item = { id: 1, name: 'hoge' }
    
    const store = new MockedVuexStore(state, mutations, actions)

    store.dispatch('add', item).then(res => {
      expect(store.state.items.length).toBe(1)
    })
  })
})
```
mutation、action、同期、非同期関わらず割と見通しの良いテストコードではないでしょうか。

#さっそく書いていきます。

[Vuex.Store](https://vuex.vuejs.org/ja/api/#vuex-store)クラスをモックしたものをここでは`MockedVuexStore`クラスとします。

`MockedVuexStore`クラスに期待するは、
- テストケース毎に独立したStateを持てる
- `commit(mutationName, payload)`メソッドからmutationを発火させる
- `dispatch(actionName, payload)`メソッドからactionを発火させる

てところでしょうか。

下記がクラス本文です。

```js:mocks.js
export class MockedVuexStore {
  constructor(state, mutations, actions) {
    this.state = state
    this.mutations = {
      ...mutations
    }
    this.actions = {
      ...actions
    }
  }

  commit(type, payload = null) {
    try {
      this.mutations[type](this.state, payload)
    } catch (e) {
      if (!e instanceof TypeError) {
        throw e
      }
      console.log(`unknown mutation "${type}" is called`)
    }
  }

  dispatch(type, payload = null) {
    try {
      return this.actions[type](
        {
          commit: (type, payload) => this.commit(type, payload)
        },
        payload
      )
    } catch (e) {
      if (!e instanceof TypeError) {
        throw e
      }
      console.log(`unknown action "${type}" is called`)
    }
  }
}
```

コンストラクタの引数としてstate, mutations, actionsを外部から受け取っております。
- stateは、テストケース内で定義されたもの
- mutationsは、テスト対象モジュールのmutation
- actionsは、テスト対象モジュールのaction
です。

`dispatch`メソッド内の

```js
return this.actions[type](
  {
    commit: (type, payload) => this.commit(type, payload)
  },
  payload
)
```
ですが、呼び出し先のactionにおいて`getters`,`dispatch`,`state`が使用される場合は、それらについて適宜モックする必要があります。（今回は言及しません。）

ともあれこれで私たちはシンプルにモック化されたVuex.Storeを手に入れることができました。

#テスト書きます。
肥大化していく複雑さに対抗しうるはテストの存在。
[Vuex](https://vuex.vuejs.org/ja/)にてテストを欲するはmutationsとactions。
テストフレームワークには手に馴染む[Jest](https://jestjs.io/ja/)。
モックされるはVuex.Store
いざ書かん。

```js:Success.test.js
import $axios from '../store/HttpClient'
import { mutations, actions } from '../store/Models/User'
import { MockedVuexStore } from './mocks'

/* axiosをモック */
jest.mock('../store/HttpClient')

describe('Test your Vuex module', () => {

  test('mutationでstateを初期化', () => {
    const state = {
      items: []
    }

    const items = [
      { id: 1, name: 'hoge' }
    ]

    const store = new MockedVuexStore(state, mutations, actions)
    store.commit('setItems', items)

    expect(store.state.items).toEqual(items)
  })

  test('非同期なactionで要素を追加', () => {
    const state = {
      items: []
    }

    const item = { id: 1, name: 'hoge' }

    /* axiosのメソッドをモック */
    $axios.post.mockResolvedValue({
      data: item,
      status: 200
    })

    const store = new MockedVuexStore(state, mutations, actions)

    store.dispatch('add', item).then(res => {
      expect(store.state.items.length).toBe(1)
    })
  })
})
```

そして成功する我がテスト。

なお、外部のAPIを叩くのに使っている（後述のUserストア内で使用しています。）[axios](https://github.com/axios/axios)はJestの[Mock Functions ](https://jestjs.io/docs/ja/mock-function-api)でモックしています。

```js
import $axios from '../store/HttpClient'

/* axiosをモック */
jest.mock('../store/HttpClient')

/* axiosのpostメソッドをモック */
$axios.post.mockResolvedValue({
  data: item,
  status: 200
})
```

###その他もろもろ

<details><summary>ディレクトリ構成</summary><div>

```
.
├── babel.config.js
├── jest.config.js
├── node_modules
├── package.json
├── store
│   ├── HttpClient.js
│   ├── Models
│   │   ├── User.js
│   │   └── index.js
│   └── index.js
├── tests
│   ├── Failure.test.js
│   ├── Success.test.js
│   ├── User.test.js
│   └── mocks.js
└── yarn.lock
```

</div></details>

<details><summary>テスト対象のUserストア</summary><div>

```js:./store/Models/User.js

import $axios from '../HttpClient'

const state = {
  items: []
}

const getters = {}

export const mutations = {
  setItems(state, data) {
    state.items = data
  },

  addItem(state, data) {
    state.items.push(data)
  },

  updateItem(state, data) {
    const index = state.items.findIndex(item => item.id == data.id)
    Object.assign(state.items[index], data)
  },

  deleteItem(state, id) {
    const index = state.items.findIndex(item => item.id == id)
    state.items.splice(index, 1)
  }
}

export const actions = {
  async fetch(context) {
    const response = await $axios.get('/api/users')
    context.commit('setItems', response.data)
    return 'OK'
  },

  async add(context, data) {
    const response = await $axios.post(`/api/users/add`, data)
    context.commit('addItem', response.data)
    return 'OK'
  },

  async edit(context, data) {
    const response = await $axios.post(`/api/users/${data.id}/edit`, data)
    context.commit('updateItem', response.data)
    return 'OK'
  },

  async delete(context, id) {
    await $axios.delete(`/api/users/${id}/delete`)
    context.commit('deleteItem', id)
    return false
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}
```
</div></details>

<details><summary>HTTP通信用のaxiosインスタンス</summary><div>

```js:./store/HttpClient.js
import axios from 'axios'

let baseURL = '/'
const $axios = axios.create({
  headers: {
    'X-Requested-With': 'XMLHttpRequest'
  },
  baseURL: baseURL
})

export default $axios
```

</div></details>

#終わりに
なるべくいつものVuexに近い形でテストが実行できるようにモックしてみました。
今後の課題としては、
- {root: true}なcommitやdispatchへの対応
- TypeScriptによる型安全な開発

あたりを頑張ればより複雑なケースに対しても対応できるのかなあとと思います。
本記事で使用したコードは[ここ](https://github.com/kyhei/jest-vuex)にあります。

ありがとうございました！