Fusic Tech Blog

Fusicエンジニアによる技術ブログ

Vuexが絡んだVueコンポーネントのテスト
2024/03/25

Vuexが絡んだVueコンポーネントのテスト

VueのコンポーネントテストといえばVue Test Utils を使うと思いますが、今回はVuexが絡んだ際のテスト方法をご紹介したいと思います。

動作、ソースコードは以下リンクで確認できます。

Edit Vue Template

カウンターアプリ

例としてカウンターアプリを作ります。

以下のようなCounter.vueというコンポーネントがあったとします。

<template>
  <div>
    <p>{{ count }}</p>
    <button class="plus-button" @click="increment">+</button>
    <button class="minus-button" @click="decrement">-</button>
  </div>
</template>

<script>
import { mapState, mapMutations } from "vuex";
export default {
  name: "Counter",
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapMutations(['increment', 'decrement'])
  }
};
</script>

動作としては「+」ボタンと「-」ボタンがあってそれぞれをクリックすると表示している数字が増えたり減ったりするというだけのアプリです。

Vuex側の処理としては以下です。

// store.js
 
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0,
    clicks: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    decrement(state) {
      state.count--;
    }
  }
});

export default store;

count というstateが一つと、incrementdecrement というmutationが二つあるだけです。

こういったgetter、mutation、actionなど、Vuexが絡んでくるテスト方法を次から説明していきます。

テスト環境

今回のテストでの環境としては以下になります。

テストランナーはJestになります。

  • Vue: 2.5.2
  • Vuex: 3.0.1
  • Vue Test Utils: 1.0.0-beta.25
  • Jest: ^22.0.4

セットアップ方法では以下記事で説明しています。

Vue Test Utils + Jest でVue.jsの単体テストを行う

テストを書いてみる

// Counter.spec.js
import { shallowMount, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import Counter from "../components/Counter";

const localVue = createLocalVue();
localVue.use(Vuex);

describe("Counter.vue", () => {
  let store;
  let mutations;

  beforeEach(() => {
    // mutationのモックを定義
    mutations = {
      increment: jest.fn(),
      decrement: jest.fn()
    };
    store = new Vuex.Store({
      state: {
        count: 0
      },
      mutations
    });
  });

  // 「+」ボタンのクリックテスト
  it('commits "increment" when "+" button is clicked', function() {
    const wrapper = shallowMount(Counter, {
      store,
      localVue
    });
    wrapper.find(".plus-button").trigger("click");
    expect(mutations.increment).toHaveBeenCalled();
  });

  // 「-」ボタンのクリックテスト
  it('commits "decrement" when "-" button is clicked', function() {
    const wrapper = shallowMount(Counter, {
      store,
      localVue
    });
    wrapper.find(".minus-button").trigger("click");
    expect(mutations.decrement).toHaveBeenCalled();
  });
});

ポイントとしては、 mutations のモックを定義してVuexインスタンスにセットしていることだけです。

今回はmutationのみでしたが、getterもactionも同じようにして書けます。

詳しくはドキュメントに書かれています。

https://vue-test-utils.vuejs.org/guides/#testing-vuex-in-components

モジュールのテスト

Vuexをモジュール化して使っている場合のテストはどうかというと、

例えば

// counter.js

// State
const state = {
  count: 0
}

// Getters
const getters = {
}

// Actions
const actions = {
}

// Mutations
const mutations = {
  increment(state) {
    state.count++;
  },
  decrement(state) {
    state.count--;
  }
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}
// store.js
import counter ./modules/counter.js

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    counter
  }
});

export default store;

とモジュール化され、

<template>
  <div>
    <p>{{ count }}</p>
    <button class="plus-button" @click="increment">+</button>
    <button class="minus-button" @click="decrement">-</button>
  </div>
</template>

<script>
import { mapState, mapMutations } from "vuex";
export default {
  name: "Counter",
  computed: {
    ...mapState('counter', ['count'])
  },
  methods: {
    ...mapMutations('counter', ['increment', 'decrement'])
  }
};
</script>

となった場合でも、

// Counter.spec.js
import { shallowMount, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import Counter from "../components/Counter";

const localVue = createLocalVue();
localVue.use(Vuex);

describe("Counter.vue", () => {
  let store;
  let counterMutations;

  beforeEach(() => {
    // counterモジュールのmutationモックを定義
    counterMutations = {
      increment: jest.fn(),
      decrement: jest.fn()
    };
    store = new Vuex.Store({
      modules: {
        counter: {
          state: {
            count: 0
          },
          mutations: counterMutations      
        }
      }
    });
  });

  // 「+」ボタンのクリックテスト
  it('commits "increment" when "+" button is clicked', function() {
    const wrapper = shallowMount(Counter, {
      store,
      localVue
    });
    wrapper.find(".plus-button").trigger("click");
    expect(counterMutations.increment).toHaveBeenCalled();
  });

  // 「-」ボタンのクリックテスト
  it('commits "decrement" when "-" button is clicked', function() {
    const wrapper = shallowMount(Counter, {
      store,
      localVue
    });
    wrapper.find(".minus-button").trigger("click");
    expect(counterMutations.decrement).toHaveBeenCalled();
  });
});

とcounterモジュールのモックを定義してやれば、テスト可能です。

そしてコンポーネントとは別にVuex側のgetter、mutation、actionをそれぞれテストしていくことで単体テストを行なっていきます。

https://vue-test-utils.vuejs.org/guides/#testing-vuex-in-components

ここについては普通のJSのテストと同じ書き方なので特に説明はしません。

最後に

Vue Test Utilsをうまく使うことで、Vuexが絡んでくるコンポーネントでもVuex側のロジックと分離してテストすることができました。

さらにJestの機能であるスナップショットテスト を利用してDOMレベルでのテストも書いていくことで、安心したVueの開発ライフを送っていきたいです。

Daiki Urata

Daiki Urata

フロントエンド/モバイルアプリなどを主に開発しています。