カテゴリー: FrontEnd

【Vue.js】コンポーネントのテストコードをvue-test-utilsとJestで実装

はじめに

今回の記事では、 Vue.js で単体テストを実装する方法を紹介します。 vue-test-utils を使うとテストコードを簡単に実装できるので、これを使っていきます。

Vue CLI でプロジェクトを作成した場合、テストランナーは Mocha + Chai か Jest のどちらか好きな方を選ぶことができますが、今回は Jest を使用します。どのテストランナーが良いかについてはこちらが参考になります。

参考までに、今回使用した主なライブラリ等のバージョンを記載します。

  • Node.js v13.2.0
  • yarn 1.17.3
  • Vue CLI 4.1.1
  • @vue/test-utils 1.0.0-beta.31
  • @vue/cli-plugin-unit-jest 4.3.1

セットアップ

Vue CLI を利用してプロジェクトを作成する場合、対話形式で選択すれば設定が自動で完了します。今回は、 Unit test に Jest 、ほかにも、 TypeScript を使うように選択しました。

コンポーネントのテスト

実際にテストコードを書く前にコンポーネントがテストの流れを簡単に説明します。

まず初めに、 vue-test-utils から mount メソッドとテスト対象のコンポーネントをインポートします。

import { mount } from '@vue/test-utils'
import MyComponent from '@/MyComponent.vue'

次に、コンポーネントのラッパーを作成します。

describe('MyComponent', () => {
  // コンポーネントがマウントされ、ラッパーが作成されます。
  const wrapper = mount(MyComponent)
}

最後にテスト内容を記述します。

describe('MyComponent', () => {
  // 省略
  it('renders hoge', () => {
    expect(wrapper.html()).toContain('<p>hoge</p>')
  })
}

なお、 MyComponent の中身は以下のとおりです。

<template>
  <div class="hello">
    <p>{{ text }}</p>
  </div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

@Component
export default class MyComponent extends Vue {
  @Prop() private text!: string;

  onClickBtn() {
    alert("hoge")
  }
}
</script>

Shallow 描画

単体テストの場合、テスト対象のコンポーネントに焦点を当ててテストするために、子コンポーネントの振る舞いに影響されたくないと思いますが、 mount のかわりに shallowMount を使うことで子コンポーネントを(スタブによって)描画しないでテストすることができます。

const wrapper = shallowMount(MyComponent)

基本的な例

Vue CLIでプロジェクトを作成した場合、以下のようなテストコードのサンプルが作成されます。

import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";

describe("HelloWorld.vue", () => {
  it("renders props.msg when passed", () => {
    const msg = "new message";
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    });
    expect(wrapper.text()).toMatch(msg);
  });
});

ちなみに、テストコードは tests/unit/ 以下に xxx.spec.ts もしくは、 xxx.test.ts という形式のファイル名で作成します。

テストを実行する場合は yarn test:unit を実行します。

$ yarn test:unit
yarn run v1.17.3
$ vue-cli-service test:unit
 PASS  tests/unit/example.spec.ts
  HelloWorld.vue
    ✓ renders props.msg when passed (22ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.713s
Ran all test suites.
✨  Done in 3.52s.

プロパティを渡す

コンポーネントにプロパティを渡すには propsData オプションを使います。

import { shallowMount } from "@vue/test-utils";
import MyComponent from "@/components/MyComponent.vue";

describe("MyComponent", () => {
  it("text is hoge", () => {
    const wrapper = shallowMount(MyComponent, {
      propsData: { text: 'hoge' }
    });
  });
});

また、 setProps メソッドでプロパティをあとから変更することもできます。

const wrapper = shallowMount(MyComponent, {
  propsData: { text: 'hoge' }
  wrapper.setProps({ text: 'fuga' })
});

ユーザーの操作をシミュレーションする

ボタンをクリックした場合など、ユーザーの操作をシミュレーションする事ができます。

この場合、まず、 find メソッドでボタン要素を探し、 trigger メソッドでクリック操作をシミュレーションします。

it('button click', () => {
  const wrapper = shallowMount(MyComponent);
  const button = wrapper.find('button')
  button.trigger('click')
})

イベントを検証する

マウントされた vue インスタンスに対して $emit メソッドを使うことでイベントを発行することができます。

it('emit event', () => {
  const wrapper = shallowMount(MyComponent);
  wrapper.vm.$emit('myevent', 'hoge')
})

発行されたイベントを検証するには emitted を使います。

it('emit event', () => {
  const wrapper = shallowMount(MyComponent);
  wrapper.vm.$emit('myevent', 'hoge')
  expect(wrapper.emitted().myevent).toBeTruthy()
  expect(wrapper.emitted().myevent[0]).toEqual(['hoge'])
})

グローバルプラグインとミックスイン

グローバルプラグイン(例えば VueRouter や Vuex など)やミックスインに依存したコンポーネントに対し、グローバルな Vue コンストラクタを汚染することなく独立した設定でテストしたい場合、 createLocalVue メソッドを使用することで実現することができます。

公式のサンプルコードを元に説明します。テストするコンポーネントは以下のとおりです。

<template>
  <div class="text-align-center">
    <input type="text" @input="actionInputIfTrue" />
    <button @click="actionClick()">Click</button>
  </div>
</template>

<script>
  import { mapActions } from 'vuex'

  export default {
    methods: {
      ...mapActions(['actionClick']),
      actionInputIfTrue: function actionInputIfTrue(event) {
        const inputValue = event.target.value
        if (inputValue === 'input') {
          this.$store.dispatch('actionInput', { inputValue })
        }
      }
    }
  }
</script>

次に、テストコードは以下のとおりです。

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Actions.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      actionClick: jest.fn(),
      actionInput: jest.fn()
    }
    store = new Vuex.Store({
      state: {},
      actions
    })
  })

  it('dispatches "actionInput" when input event value is "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'input'
    input.trigger('input')
    expect(actions.actionInput).toHaveBeenCalled()
  })

  it('does not dispatch "actionInput" when event value is not "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'not input'
    input.trigger('input')
    expect(actions.actionInput).not.toHaveBeenCalled()
  })

  it('calls store action actionClick when button is clicked', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
  })
})

以下のコードで拡張された Vue コンストラクタを作成し、 Vuex をインストールします。

const localVue = createLocalVue()

localVue.use(Vuex)

以下のコードで Vuex.store のモックを作成します。これは beforeEach でテスト前に毎回作成されます。

beforeEach(() => {
  actions = {
    actionClick: jest.fn(),
    actionInput: jest.fn()
  }
  store = new Vuex.Store({
    state: {},
    actions
  })
})

以下のコードで作成した Vuex.store のモックを使えるようにします。

const wrapper = shallowMount(Actions, { store, localVue })

wrapperのプロパティとメソッド

wrapper オブジェクトのプロパティとメソッドの一部を紹介します。詳細はこちらをご覧ください。

vm vueインスタンスで、プロパティとメソッドにアクセスできる
element ラッパーのルートDOM
attributes ラップされている要素の属性(オブジェクト)
classes ラップされている要素のクラス名
contains 要素もしくはセレクタで指定したコンポーネントを含んでいるか
exist 存在するか
find 最初のDOMノードのwrapperもしくはVueコンポーネント
trigger DOMノードのイベントを発火する

Jestのメソッド

Jest のメソッドの一部を紹介します。詳細はこちらをご覧ください。

expect 値をテストしたい時に外のマッチャー関数と一緒に使用する
toBe 値もしくはオブジェクトのいくつかのプロパティを検証する
toBeTruety bool値でtrueであるか
toBeFalsy bool値でfalseであるか
toContain アイテムが配列内にあるか
toMatch 文字列が正規表現と一致するか

さいごに

いかがでしたでしょうか。今回は基礎的なところでコンポーネントの単体テストに絞って紹介しました。この他にも、実際のプロジェクトでは axios などの非同期処理や Vuex などもテストしたい場合があると思いますので、今後機会があれば調べて見たいと思います。

おすすめ書籍

   

Hiroki Ono

シェア
執筆者:
Hiroki Ono
タグ: TypeScriptVuejs

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前