💡

Vue Test Utils: コンポーネントのスタブ化とイベント発火方法

2020/11/10に公開

はじめに

Vue.js や Nuxt のプロジェクトで Vuetify や Buefy などのライブラリを使用することは珍しくないですね。しかし、そういったライブラリのコンポーネントを含んだファイルをテストすると、「そんなコンポーネントはない」というエラーが出てしまいます。
エラー画面

これを回避するために、ライブラリのサイトなどではテスト時にもプラグインを読み込むように書かれていることが多いです。Vuetify のドキュメント
しかし、この方法は少々問題を含んでいるのです。

ドキュメントに沿ったテスト記述例

ここで「ボタンを押したら数字が増える」というシンプルなコンポーネントを例にしてみましょう。

  • Vuetify のボタンコンポーネントを使用(別ファイルで Vuetify をプラグイン登録している想定)
  • なるべく単純にするため従来の Options API で記述し、テンプレート内のイベントハンドラで変数の値を変更
  • テスト時に要素を特定するために data-test を付与
<template>
  <div>
    <v-btn data-test="btn" @click="count++">count: {{ count }}</v-btn>
  </div>
</template>

<script>
export default {
  data: () => ({
    count: 0
  })
};
</script>

前述の Vuetify のドキュメントを参考に、このコンポーネントに対するテストを Jest で書くと以下のようになります。

// setup.js
import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify) // 1. グローバルにプラグイン登録

// テストファイル
import { createLocalVue, mount } from "@vue/test-utils";
import Vuetify from "vuetify";
import App from "@/App.vue";

describe("App.vue", () => {
  const localVue = createLocalVue();
  let vuetify;

  beforeEach(() => {
    vuetify = new Vuetify(); // 2. テストごとに初期化
  });

  it("ボタンをクリックするとカウントが増える", () => {
    const wrapper = mount(App, { // 3. mount 関数を使用
      localVue,
      vuetify
    });

    const btn = wrapper.find(`[data-test="btn"]`);

    // trigger でイベント発火
    btn.trigger("click");

    expect(wrapper.vm.$data.count).toBe(1);
  });
});

この方法にはいくつか懸念点があります。

  1. テストがグローバル登録したプラグインに依存している
  2. テストケースごとに初期化の処理が入る(テストが増えると重くなっていきそう)
  3. mount 関数なので、子孫コンポーネントの処理が走る
    • 計算量が増えるのでテスト時間が伸びる
    • 子の中で API を呼んだり Vuex を使っている場合、それらのモック化が必要になる(テスト内容と直接関係ないコードを書かないといけない。テストが子の実装に依存してしまう)
    • vue-test-utils のガイドでも shallowMount が推奨されている

※ ローカルでプラグイン登録できるライブラリもあり、その場合、上記の 1 はなくなります。
初期化の処理はテストファイルごとに行なわれるので、2 も多少は軽減されます。[1]

スタブ化する場合のテスト記述例

これらの問題を解決するためプラグインの読み込みをやめて、子コンポーネントはスタブ化します。
さらに mount 関数ではなく shallowMount 関数を使用することで、テストが子コンポーネントの実装に依存しなくなり、テスト実行時間の短縮も見込めます。

import { createLocalVue, shallowMount } from "@vue/test-utils";
import App from "@/App.vue";

describe("App.vue", () => {
  const localVue = createLocalVue();

  it("ボタンをクリックするとカウントが増える", async () => {
    const wrapper = shallowMount(App, { // 1. shallowMount に変更
      localVue,
      stubs: { // 2. stubs オプション追加
        "v-btn": true
      }
    });

    const btn = wrapper.find(`[data-test="btn"]`);

    btn.trigger("click");
    expect(wrapper.vm.$data.count).toBe(0); // 3. trigger は効かなくなる(0 のまま)

    btn.vm.$emit("click");
    expect(wrapper.vm.$data.count).toBe(1); // 4. emit すると増える
  });
});
  1. shallowMount 関数に変更
  2. 第 2 引数に stubs を追加(公式ドキュメント
  3. スタブ化すると trigger メソッドは効かない
  4. vm の $emit メソッドを呼び出すとイベントを発火できる

まとめ

上記のとおり、スタブ化すると trigger が効かなくなってしまうのですが、その解決方法が分からず、かなりの時間を使って検索+試行錯誤した記憶があります。
答えが分かってしまえば簡単なことなのですが、なかなか思い至らないんですよね……。

脚注
  1. localVue.use(Buefy) のように書きます ↩︎

Discussion