Top View


Author Daiki Urata

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

2018/11/08

カウンターアプリ

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

以下のような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

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

テストを書いてみる

// 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も同じようにして書けます。

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

モジュールのテスト

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をそれぞれテストしていくことで単体テストを行なっていきます。

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

最後に

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

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

Daiki Urata

Daiki Urata

Twitter X

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