7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-07-08

はじめに

肥大化していく複雑さに対抗しうるはテストの存在。
Vuexにてテストを欲するはmutationsとactions。
テストフレームワークには手に馴染むJest
いざ書かん。

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
  })
})

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

上記のテストでは1つ目のテストは成功するものの、2つ目のテストでは配列の要素数が期待どおりではなく失敗しています。
Vuex自体の動作としては望ましいですが、stateの状態をテストスイート全体で追いかけるのは不便で面倒です。
公式のチュートリアルではmutationやactionを個別でインポートし、stateをモックすることでテストを行っています。)

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

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

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

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クラスをモックしたものをここではMockedVuexStoreクラスとします。

MockedVuexStoreクラスに期待するは、

  • テストケース毎に独立したStateを持てる
  • commit(mutationName, payload)メソッドからmutationを発火させる
  • dispatch(actionName, payload)メソッドからactionを発火させる

てところでしょうか。

下記がクラス本文です。

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メソッド内の

return this.actions[type](
  {
    commit: (type, payload) => this.commit(type, payload)
  },
  payload
)

ですが、呼び出し先のactionにおいてgetters,dispatch,stateが使用される場合は、それらについて適宜モックする必要があります。(今回は言及しません。)

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

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

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はJestのMock Functions でモックしています。

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

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

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

###その他もろもろ

ディレクトリ構成
.
├── 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
テスト対象のUserストア
./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
}
HTTP通信用のaxiosインスタンス
./store/HttpClient.js
import axios from 'axios'

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

export default $axios

#終わりに
なるべくいつものVuexに近い形でテストが実行できるようにモックしてみました。
今後の課題としては、

  • {root: true}なcommitやdispatchへの対応
  • TypeScriptによる型安全な開発

あたりを頑張ればより複雑なケースに対しても対応できるのかなあとと思います。
本記事で使用したコードはここにあります。

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

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?