10
13

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.

Vue.js+Vuexで作成したカウンターアプリのユニットテストを作成する

Posted at

概要

Vue.js+Vuexを利用したカウンターアプリのテストクラスを作成します。テストツールはJestを使用。

この記事では、カウンターアプリのテストとして適切であるかという点よりも、Vue.js+Vuexのテストのサンプルとなるように意識して書いています。
これからテストを書こうと思っている方の参考になれば幸いです。

テスト対象

今回のテストの対象となるコンポーネントとカウンターモジュールです。

Counterコンポーネント

カウントされる数値とincrement!decrement!ボタンが表示されています。

Counter.vue
<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のデータフローをわかりやすくするために、このような形にしています。

counter.ts
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
CounterModule.spec.ts
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を使用。

Cunter.spec.ts
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

10
13
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
10
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?