概要
Vue.js+Vuexを利用したカウンターアプリのテストクラスを作成します。テストツールはJest
を使用。
この記事では、カウンターアプリのテストとして適切であるかという点よりも、Vue.js+Vuexのテストのサンプルとなるように意識して書いています。
これからテストを書こうと思っている方の参考になれば幸いです。
テスト対象
今回のテストの対象となるコンポーネントとカウンターモジュールです。
Counterコンポーネント
カウントされる数値とincrement!
と decrement!
ボタンが表示されています。
<template>
<div>
<p>{{ counter }}</p>
<button @click="increment">increment!</button>
<button @click="decrement">decrement!</button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { Getter, Action, namespace } from "vuex-class";
import { CounterState } from "../types/counter";
const CounterGetter = namespace("CounterModule", Getter);
const CounterAction = namespace("CounterModule", Action);
@Component
export default class Counter extends Vue {
@CounterGetter
counter!: CounterState["counter"];
@CounterAction
increment!: () => void;
@CounterAction
decrement!: () => void;
}
</script>
Counterモジュール
VuexStoreをモジュールに分割し、CounterModuleを作成しました。
Actionは単にMutationをcommitするだけのものとなっていますが、Vuexのデータフローをわかりやすくするために、このような形にしています。
import Vue from "vue";
import Vuex, { GetterTree, MutationTree, ActionTree, Module } from "vuex";
import { CounterState } from "@/types/counter";
import { RootState } from "@/store";
Vue.use(Vuex);
const state: CounterState = {
counter: 0
};
const getters: GetterTree<CounterState, RootState> = {
counter: (state): CounterState["counter"] => {
return state.counter;
}
};
const mutations: MutationTree<CounterState> = {
increment: state => {
state.counter += 1;
},
decrement: state => {
state.counter -= 1;
}
};
const actions: ActionTree<CounterState, RootState> = {
increment: ({ commit }) => {
commit("increment");
},
decrement: ({ commit }) => {
commit("decrement");
}
};
export const CounterModule: Module<CounterState, RootState> = {
namespaced: true,
state,
mutations,
actions,
getters
};
テスト観点
- VuexStore (CounterModule)
- コンポーネント (Counterコンポーネント)
VuexStoreのテストは、getterやmutation、actionのロジック単体を検証します。
コンポーネントのテストは、コンポーネントがVuexStoreのstateの状態に基づいて正しく動作していてることを検証します。VuexStoreのmutationやactionの内部ロジックについては、VuexStoreのテストで検証されるので、コンポーネントのテストでは検証しません。
VuexStore(CounterModule)のテスト
まず、tests/unit/CounterModule.spec.ts
を作成します。
拡張子に.spec.js
をつけることで、Jestでテストが実行されます。
以下についてテストを行います。
- Getter
- Mutation
- Action
import { CounterState } from "@/types/counter";
import { CounterModule } from "@/store/modules/counter";
describe("CounterModule", () => {
describe("getters", () => {
it("should be able to get counter", () => {
const state: CounterState = {
counter: 1
};
const wrapper = (getters: any) => getters.counter(state);
const counter: CounterState = wrapper(CounterModule.getters);
expect(counter).toEqual(state.counter);
});
});
describe("mutations", () => {
let state: CounterState;
beforeEach(() => {
state = {
counter: 1
};
});
it("should be able to increment counter", () => {
const wrapper = (mutations: any) => mutations.increment(state);
wrapper(CounterModule.mutations);
expect(state.counter).toEqual(2);
});
it("should be able to decrement counter", () => {
const wrapper = (mutations: any) => mutations.decrement(state);
wrapper(CounterModule.mutations);
expect(state.counter).toEqual(0);
});
});
describe("actions", () => {
let state: CounterState;
beforeEach(() => {
state = {
counter: 1
};
});
it("should be able to commit increment", () => {
const commit = jest.fn();
const wrapper = (actions: any) => actions.increment({ commit });
wrapper(CounterModule.actions);
expect(commit).toHaveBeenCalledWith("increment");
});
it("should be able to commit decrement", () => {
const commit = jest.fn();
const wrapper = (actions: any) => actions.decrement({ commit });
wrapper(CounterModule.actions);
expect(commit).toHaveBeenCalledWith("decrement");
});
});
});
Getter
getterで取得した値がstateと一致していることを検証。
補足:getter、mutation、actionのテストに共通しているのですが、
以下のコードは、TypeScriptのエラーを回避するため、強引にany型にキャストしています。
const wrapper = (getters: any) => getters.counter(state);
const counter: CounterState = wrapper(CounterModule.getters);
以下は、テストは通過しますが、TypeScriptでエラーが表示されます。
CounterModule.actions.increment({ commit });
Mutation
beforeEach()
でstateを初期化し、mutationを実行することでstateの値が変更されていることを検証。
Action
Storeのcommitをjest.fn()
でモック化しています。
expect(commit).toHaveBeenCalledWith("increment");
でactionがmutationをコミットしていることを検証。
コンポーネントのテスト
ここでは、以下の3点をテストを行います。
- カウントの数値の表示
- incrementボタン押下の際のActionの呼び出し
- decrementボタン押下の際のActionの呼び出し
ライブラリにはvue-test-utils
を使用。
import { shallowMount, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
import Counter from "@/components/Counter.vue";
import { CounterModule } from "@/store/modules/counter";
import { CounterState } from "@/types/counter";
const localVue = createLocalVue();
localVue.use(Vuex);
describe("Counter.vue", () => {
let store: any;
let state: CounterState;
let actions: any;
beforeEach(() => {
state = {
counter: 1
};
actions = {
increment: jest.fn(),
decrement: jest.fn()
};
store = new Vuex.Store({
modules: {
CounterModule: {
namespaced: true,
state,
actions,
getters: CounterModule.getters
}
}
});
});
it('renders "state.counter" in first p tag', () => {
const wrapper = shallowMount(Counter, { store, localVue });
const p = wrapper.find("p");
expect(p.text()).toBe(state.counter.toString());
});
it('calls store action "increment" when increment button is clicked', () => {
const wrapper = shallowMount(Counter, { store, localVue });
const button = wrapper.findAll("button").at(0);
button.trigger("click");
expect(actions.increment).toHaveBeenCalled();
});
it('calls store action "decrement" when decrement button is clicked', () => {
const wrapper = shallowMount(Counter, { store, localVue });
const button = wrapper.findAll("button").at(1);
button.trigger("click");
expect(actions.decrement).toHaveBeenCalled();
});
});
テストのポイント
VuexStoreのモジュールのテストを可能にするため、CounterModuleを作成しています。
ActionはJestのモック関数jest.fn()
を使用し、ボタンが押下された際に、actions.increment
またはactions.decrement
が呼び出されたことを検証しています。
参考記事
以下の記事を参考にしています。
Vue Test Utils
Vue testing handbook