Testing Vuex

Vuex は単なる実装の詳細であり、Vuex を使用したコンポーネントのテストに特別な扱いは必要ありません。とはいえ、テストを読みやすく、書きやすくするためのテクニックはいくつかあります。ここでは、それらについて見ていきます。

このガイドは、あなたが Vuex に精通していることを前提にしています。Vuex 4 は、Vue.js 3 で動作するバージョンです。ドキュメントは こちら をご覧ください。

簡単な例

ここでは、単純な Vuex ストアと、Vuex ストアが存在することに依存するコンポーネントを紹介します:

import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      count: 0
    }
  },
  mutations: {
    increment(state: any) {
      state.count += 1
    }
  }
})

ストアは単純にカウントを保存し、increment 変異がコミットされたときにそれを増加させます。これは私たちがテストするコンポーネントです:

const App = {
  template: `
    <div>
      <button @click="increment" />
      Count: {{ count }}
    </div>
  `,
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}

実際の Vuex ストアを使ったテスト

このコンポーネントと Vuex ストアが動作していることを完全にテストするために、<button> をクリックしてカウントが増加することを確認します。Vue アプリケーションでは、通常 main.js で、このように Vuex をインストールします:

const app = createApp(App)
app.use(store)

これは、Vuex がプラグインであるためです。プラグインは、app.use を呼び出してプラグインを渡すことで適用されます。

Vue Test Utils では、global.plugins のマウントオプションを使用して、プラグインもインストールすることができます。

import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      count: 0
    }
  },
  mutations: {
    increment(state: any) {
      state.count += 1
    }
  }
})

test('vuex', async () => {
  const wrapper = mount(App, {
    global: {
      plugins: [store]
    }
  })

  await wrapper.find('button').trigger('click')

  expect(wrapper.html()).toContain('Count: 1')
})

プラグインをインストールした後、ボタンをクリックする trigger を使って、count が増加したことをアサートします。この種のテストは、異なるシステム間 (この場合はコンポーネントとストア) の相互作用をカバーするもので、統合テストとして知られています。

モックストアを使ったテスト

対照的に、ユニットテストはコンポーネントとストアを別々に分離してテストするかもしれません。これは、複雑なストアを持つ非常に大きなアプリケーションを持っている場合に便利です。このような場合、global.mocks を使ってストアの一部だけをモックします:

test('vuex using a mock store', async () => {
  const $store = {
    state: {
      count: 25
    },
    commit: jest.fn()
  }

  const wrapper = mount(App, {
    global: {
      mocks: {
        $store
      }
    }
  })

  expect(wrapper.html()).toContain('Count: 25')
  await wrapper.find('button').trigger('click')
  expect($store.commit).toHaveBeenCalled()
})

本物の Vuex ストアを使用して global.plugins 経由でインストールするのではなく、独自のモックストアを作成し、コンポーネントで使用する Vuex の部分(この場合、statecommit の関数)だけを実装しました。

ストアを分離してテストするのは便利だと思われるかもしれませんが、Vuex ストアを壊しても何の警告も出ないことに注意してください。Vuex ストアをモックするか、本物のストアを使用するかをよく検討し、トレードオフを理解してください。

Vuex を分離してテストする

Vuex の変異やアクションを完全に分離してテストしたいと思うかもしれません。それらが複雑な場合は特に。Vuex のストアは通常の JavaScript なので、Vue Test Utils は必要ありません。以下は、Vue Test Utils を使わずに increment 変異をテストする方法です:

test('increment mutation', () => {
  const store = createStore({
    state: {
      count: 0
    },
    mutations: {
      increment(state) {
        state.count += 1
      }
    }
  })

  store.commit('increment')

  expect(store.state.count).toBe(1)
})

Vuex の状態をプリセットする

Vuex ストアを特定の状態にしておくと、テストに便利なことがあります。global.mocks 以外で使える便利なテクニックは、createStore をラップして、初期状態の種となる引数を取る関数を作成することです。この例では、state.count に追加される引数を取るために incrementを拡張しています。この引数がない場合は、state.count を 1 だけ増加させます。

const createVuexStore = (initialState) =>
  createStore({
    state: {
      count: 0,
      ...initialState
    },
    mutations: {
      increment(state, value = 1) {
        state.count += value
      }
    }
  })

test('increment mutation without passing a value', () => {
  const store = createVuexStore({ count: 20 })
  store.commit('increment')
  expect(store.state.count).toBe(21)
})

test('increment mutation with a value', () => {
  const store = createVuexStore({ count: -10 })
  store.commit('increment', 15)
  expect(store.state.count).toBe(5)
})

初期状態を受け取る createVuexStore 関数を作成することで、簡単に初期状態を設定することができます。これにより、テストを簡素化しつつ、すべてのエッジケースをテストすることができます。

Vue Testing Handbook には、Vuex をテストするためのより多くの例があります。注意: この例は Vue.js 2 と Vue Test Utils v1 に関係します。考え方やコンセプトは同じで、Vue Testing Handbook は近いうちに Vue.js 3 と Vue Test Utils 2 に更新される予定です。

Composition API を使ったテスト

Vuex は Composition API を使用する場合、useStore 関数でアクセスします。詳しくはこちらをご覧ください

useStore は、Vuex のドキュメント で説明されているように、オプションでユニークなインジェクションキーを使用することができます。

このような感じです:

import { createStore } from 'vuex'
import { createApp } from 'vue'

// create a globally unique symbol for the injection key
const key = Symbol()

const App = {
  setup () {
    // use unique key to access store
    const store = useStore(key)
  }
}

const store = createStore({ /* ... */ })
const app = createApp({ /* ... */ })

// specify key as second argument when calling app.use(store)
app.use(store, key)

useStore を使うたびにキーパラメータの受け渡しを繰り返すことを避けるため、Vuex のドキュメントでは、そのロジックをヘルパー関数に抽出し、デフォルトの useStore 関数の代わりにその関数を再利用することを推奨しています。詳しくはこちらをご覧ください 。Vue Test Utils を使用してストアを提供するアプローチは、コンポーネントで useStore 関数を使用する方法によって異なります。

インジェクションキーなしで useStore を利用するコンポーネントのテスト

インジェクションキーがなければ、グローバルなマウントオプションである provide を使ってストアデータをコンポーネントに注入することができます。注入されるストアの名前は、コンポーネント内の名前と同じである必要があります (例: "store")。

キーによらない useStore を提供する場合の例

import { createStore } from 'vuex'

const store = createStore({
  // ...
})

const wrapper = mount(App, {
  global: {
    provide: {
      store: store
    },
  },
})

インジェクションキーで useStore を利用するコンポーネントをテストする

インジェクションキーでストアを使用する場合、以前のアプローチは機能しません。useStore からはストアのインスタンスは返されません。正しいストアにアクセスするために、識別子を指定する必要があります。

これは、コンポーネントの setup 関数で useStore に渡されるキー、 あるいはカスタムヘルパー関数内で useStore に渡されるキーとまったく同じである必要があります。JavaScript のシンボルは一意であり、再作成することができないので、実際のストアからキーをエクスポートするのが最善です。

ストアを注入するには、正しいキーを持つ global.provide を使用するか、ストアをインストールしキーを指定する global.plugins を使用することができます:

global.provide を使用して Keyed useStore を提供する

// store.js
export const key = Symbol()
// app.spec.js
import { createStore } from 'vuex'
import { key } from './store'

const store = createStore({ /* ... */ })

const wrapper = mount(App, {
  global: {
    provide: {
      [key]: store
    },
  },
})

global.plugins を使用した Keyed useStore の提供

// store.js
export const key = Symbol()
// app.spec.js
import { createStore } from 'vuex'
import { key } from './store'

const store = createStore({ /* ... */ })

const wrapper = mount(App, {
  global: {
    // to pass options to plugins, use the array syntax.
    plugins: [[store, key]]
  },
})

結論

  • Vuex をプラグインとしてインストールするには global.plugins を使用します。
  • 高度なユースケースのために Vuex のようなグローバルオブジェクトをモックするために global.mocks を使用します。
  • 複雑な Vuex の変異とアクションを分離してテストすることを検討します。
  • 特定のテストシナリオを設定するために、引数を取る関数で createStore をラップします。