You’re browsing the documentation for Vue Test Utils for Vue v2.x and earlier.

To read docs for Vue Test Utils for Vue 3, click here.

ガイド

はじめる

セットアップ

vue-test-utils の使い方を体験したい場合は、基本設定としてデモリポジトリをクローンし、依存関係をインストールしてください。

git clone https://github.com/vuejs/vue-test-utils-getting-started
cd vue-test-utils-getting-started
npm install

プロジェクトには単純なコンポーネント、counter.js が含まれています。

// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data() {
    return {
      count: 0
    }
  },

  methods: {
    increment() {
      this.count++
    }
  }
}

マウンティングコンポーネント

vue-test-utils は Vue コンポーネントを隔離してマウントし、必要な入力(プロパティ、注入、そしてユーザイベント)をモックし、そして出力(描画結果、カスタムイベントの発行)を検証することでテストします。

マウントされたコンポーネントは Wrapper の内部に返されます。これは、基の Vue コンポーネントインスタンスを操作、トラバース、クエリ処理するための多くの便利なメソッドを公開しています。

mount メソッドを使ってラッパを作成することができます。test.js というファイルを作りましょう:

// test.js

// test utils から mount() メソッドをインポート
// テストするコンポーネント
import { mount } from '@vue/test-utils'
import Counter from './counter'

// コンポーネントがマウントされ、ラッパが作成されます。
const wrapper = mount(Counter)

// wrapper.vmを 介して実際の Vue インスタンスにアクセスできます
const vm = wrapper.vm

// ラッパをより深く調べるためにコンソールに記録してみましょう。
// vue-test-utils でのあなたの冒険はここから始まります。
console.log(wrapper)

コンポーネントの描画された HTML 出力をテストする

ラッパが完成したので、コンポーネントの描画された HTML 出力が、期待されるものと一致することを確認します。

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

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

  it('renders the correct markup', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // 要素の存在を確認することも簡単です
  it('has a button', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})

次に、npm test でテストを実行します。テストが合格になるはずです。

ユーザのインタラクションをシミュレーションする

ユーザがボタンをクリックすると、カウンタがカウントをインクリメントする必要があります。この振る舞いをシミュレートするには、まずbutton 要素のラッパを返す wrapper.find() を使ってボタンを見つける必要があります。ボタンのラッパで .trigger() を呼び出すことでクリックをシミュレートできます:

it('button click should increment the count', () => {
  expect(wrapper.vm.count).toBe(0)
  const button = wrapper.find('button')
  button.trigger('click')
  expect(wrapper.vm.count).toBe(1)
})

nextTick はどうですか?

Vue は保留した DOM 更新をまとめて処理し、非同期に適用して、複数のデータのミューテーションに起因する不要な再描画を防ぎます。実際には、Vue が何らかの状態変更をトリガした後に Vue が実際の DOM 更新を実行するまで待つために、Vue.nextTick を使用しなければならないからです。

使い方を簡単にするため、 vue-test-utils はすべての更新を同期的に適用するので、テストで DOM を更新するために Vue.nextTick を使う必要はありません。

注意: 非同期コールバックやプロミスの解決などの操作のために、イベントループを明示的に進める必要がある場合は、まだ nextTick が必要です。

テストファイルで nextTick をまだ使う必要がある場合は、 nextTick の内部で Promise を使っているので、 nextTick 内で発生したエラーはテストランナーによって捕捉されないことに注意してください。これを解決するには 2 つの方法があります。 1 つ目はテストの最初で Vue のグローバルエラーハンドラに done コールバックをセットする方法です。2 つ目は nextTick を引数なしで実行して、それを Promise としてテストランナーに返す方法です。

// これは捕捉されない
it('will time out', done => {
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

// 以下の2つのテストは期待通り動作します
it('will catch the error using done', done => {
  Vue.config.errorHandler = done
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

it('will catch the error using a promise', () => {
  return Vue.nextTick().then(function () {
    expect(true).toBe(false)
  })
})

次は何をするのか

一般的なヒント

何をテストするかを知る

UI コンポーネントでは、コンポーネントの内部実装の詳細に集中しすぎて脆弱なテストが発生する可能性があるため、完全なラインベースのカバレッジを目指すことはお勧めしません。

代わりに、コンポーネントのパブリックインターフェイスを検証するテストを作成し、内部をブラックボックスとして扱うことをお勧めします。単一のテストケースでは、コンポーネントに提供された入力(ユーザーのやり取りやプロパティの変更)によって、期待される出力(結果の描画またはカスタムイベントの出力)が行われることが示されます。

たとえば、ボタンがクリックされるたびに表示カウンタを 1 ずつインクリメントする Counter コンポーネントの場合、そのテストケースはクリックをシミュレートし、描画された出力が 1 つ増加したのか検証します。カウンタは値をインクリメントし、入力と出力のみを扱います。

このアプローチの利点は、コンポーネントのパブリックインターフェイスが同じままである限り、コンポーネントの内部実装が時間の経過とともにどのように変化してもテストは合格になります。

このトピックは、Matt O'Connell による偉大なプレゼンテーションで詳細に説明されています。

Shallow 描画

単体テストでは、通常、単体テストとしてテスト対象のコンポーネントに焦点を当て、子コンポーネントの動作を間接的に検証することを避けたいと考えています。

さらに、多くの子コンポーネントを含むコンポーネントの場合、描画されたツリー全体が非常に大きくなる可能性があります。すべての子コンポーネントを繰り返し描画すると、テストが遅くなる可能性があります。

vue-test-utils を使うと、shallowMount メソッドを使って子コンポーネントを(スタブによって)描画せずにコンポーネントをマウントすることができます:

import { shallowMount } from '@vue/test-utils'

const wrapper = shallowMount(Component) // Component インスタンスを含む Wrapper を返します。
wrapper.vm // マウントされた Vue インスタンス

イベントの発行を検証する

マウントされた各ラッパは、基になる Vue インスタンスによって発行されたすべてのイベントを自動的に記録します。wrapper.emitted() を使って、記録されたイベントを取り出すことができます:

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

/*
wrapper.emitted()は次のオブジェクトを返します:
{
  foo: [[], [123]]
}
*/

次に、これらのデータに基づいて検証することもできます。

// イベントが発行されたか検証する
expect(wrapper.emitted().foo).toBeTruthy()

// イベント数を検証する
expect(wrapper.emitted().foo.length).toBe(2)

// イベントのペイロードを検証する
expect(wrapper.emitted().foo[1]).toEqual([123])

また、wrapper.emittedByOrder() を呼び出すことで、発行順序のイベントの配列を取得することもできます。

コンポーネントの状態を操作する

ラッパの setData メソッドまたは setProps メソッドを使って、コンポーネントの状態を直接操作することができます。:

it('manipulates state', async () => {
  await wrapper.setData({ count: 10 })

  await wrapper.setProps({ foo: 'bar' })
})

プロパティをモックする

Vue に組み込まれた propsData オプションを使用してコンポーネントにプロパティを渡すことができます:

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

mount(Component, {
  propsData: {
    aProp: 'some value'
  }
})

wrapper.setProps({}) メソッドを使用して、すでにマウントされているコンポーネントのプロパティを更新することもできます。

オプションの完全なリストについては、ドキュメントのマウントオプションのセクションを参照してください。

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

コンポーネントの中には、グローバルプラグインやミックスインによって注入された機能 (例: vuexvue-router など)に依存するものもあります。

特定のアプリケーションでコンポーネントのテストを作成している場合は、同じグローバルプラグインとミックスインをテストのエントリに設定できます。しかし、異なるアプリケーション間で共有される可能性のあるジェネリックコンポーネントスイートをテストする場合など、グローバルな Vue コンストラクタを汚染することなく、より孤立した設定でコンポーネントをテストする方が良い場合もあります。createLocalVue メソッドを使用すると、次のことができます:

import { createLocalVue } from '@vue/test-utils'

// 拡張された Vue コンストラクタを作成する
const localVue = createLocalVue()

// プラグインをインストールする
localVue.use(MyPlugin)

// localVue をマウントオプションに渡す
mount(Component, {
  localVue
})

Vue Router のようなプラグインはグローバルの Vue コンストラクタに read-only なプロパティを追加します。
これは localVue コンストラクタにそのプラグインを再びインストールすることや read-only なプロパティに対するモックを追加することを不可能にします。

モックの注入

単純なモックを注入するための別の戦略として mocks オプションで行うことができます:

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

const $route = {
  path: '/',
  hash: '',
  params: { id: '123' },
  query: { q: 'hello' }
}

mount(Component, {
  mocks: {
    $route // コンポーネントをマウントする前に、モックした $route オブジェクトを Vue インスタンスに追加します。
  }
})

スタブコンポーネント

stubs オプションを使用して、グローバルまたはローカルに登録されたコンポーネントを上書きできます:

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

mount(Component, {
  // globally-registered-component を空のスタブとして
  // 解決します
  stubs: ['globally-registered-component']
})

ルーティングの扱い

定義によるルーティングは、アプリケーションの全体的な構造と関連し、複数のコンポーネントが関係するため、統合テストまたはエンドツーエンドテストによってよくテストされます。 vue-router 機能に依存する個々のコンポーネントについては、上記の手法を使ってモックすることができます。

スタイルの検知

jsdomを使う場合、テスト対象として検知できるのはインラインで書かれたスタイルだけです。

キー、マウス、その他の DOM イベントのテスト

イベントをトリガする

Wrappertrigger メソッドで DOM イベントをトリガすることができます。

test('triggers a click', async () => {
  const wrapper = mount(MyComponent)

  await wrapper.trigger('click')
})

find メソッドは mount メソッドと同じように Wrapper を返します。 MyComponent 内に button があると仮定すると、以下のコードは、 button をクリックします。

test('triggers a click', async () => {
  const wrapper = mount(MyComponent)

  await wrapper.find('button').trigger('click')
})

オプション

trigger メソッドはオプションで options オブジェクトを引数として取ります。options オブジェクトのプロパティはイベントオブジェクトのプロパティに追加されます。

target を options オブジェクトに追加することができないことに注意してください。

test('triggers a click', async () => {
  const wrapper = mount(MyComponent)

  await wrapper.trigger('click', { button: 0 })
})

マウスクリックの例

テスト対象のコンポーネント

<template>
  <div>
    <button class="yes" @click="callYes">Yes</button>
    <button class="no" @click="callNo">No</button>
  </div>
</template>
<script>
  export default {
    name: 'YesNoComponent',
    props: {
      callMe: {
        type: Function
      }
    },
    methods: {
      callYes() {
        this.callMe('yes')
      },
      callNo() {
        this.callMe('no')
      }
    }
  }
</script>

テスト

import YesNoComponent from '@/components/YesNoComponent'
import { mount } from '@vue/test-utils'
import sinon from 'sinon'

it('Click on yes button calls our method with argument "yes"', async () => {
  const spy = sinon.spy()
  const wrapper = mount(YesNoComponent, {
    propsData: {
      callMe: spy
    }
  })
  await wrapper.find('button.yes').trigger('click')

  spy.should.have.been.calledWith('yes')
})

キーボードの例

テスト対象のコンポーネント

このコンポーネントはいくつかのキーを使用して quantity を増減することができます。

<template>
  <input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>
<script>
  const KEY_DOWN = 40
  const KEY_UP = 38
  const ESCAPE = 27

  export default {
    data() {
      return {
        quantity: 0
      }
    },
    methods: {
      increment() {
        this.quantity += 1
      },
      decrement() {
        this.quantity -= 1
      },
      clear() {
        this.quantity = 0
      },
      onKeydown(e) {
        if (e.keyCode === ESCAPE) {
          this.clear()
        }
        if (e.keyCode === KEY_DOWN) {
          this.decrement()
        }
        if (e.keyCode === KEY_UP) {
          this.increment()
        }
        if (e.key === 'a') {
          this.quantity = 13
        }
      }
    },
    watch: {
      quantity: function (newValue) {
        this.$emit('input', newValue)
      }
    }
  }
</script>

テスト

import QuantityComponent from '@/components/QuantityComponent'
import { mount } from '@vue/test-utils'

describe('Key event tests', () => {
  it('Quantity is zero by default', () => {
    const wrapper = mount(QuantityComponent)
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Cursor up sets quantity to 1', async () => {
    const wrapper = mount(QuantityComponent)
    await wrapper.trigger('keydown.up')
    expect(wrapper.vm.quantity).toBe(1)
  })

  it('Cursor down reduce quantity by 1', async () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    await wrapper.trigger('keydown.down')
    expect(wrapper.vm.quantity).toBe(4)
  })

  it('Escape sets quantity to 0', async () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    await wrapper.trigger('keydown.esc')
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Magic character "a" sets quantity to 13', async () => {
    const wrapper = mount(QuantityComponent)
    await wrapper.trigger('keydown', {
      key: 'a'
    })
    expect(wrapper.vm.quantity).toBe(13)
  })
})

制限事項

. の後のキー名( keydown.up の場合 up )は keyCode に変換されます。以下のキー名が変換されます。

キー名 キーコード
enter 13
esc 27
tab 9
space 32
delete 46
backspace 8
insert 45
up 38
down 40
left 37
right 39
end 35
home 36
pageup 33
pagedown 34

重要事項

vue-test-utils は同期的にイベントをトリガします。従って、 Vue.nextTick() を実行する必要はありません。

非同期動作のテスト

テストをシンプルにするために、 vue-test-utils は DOM の更新を同期的に適用します。しかし、コールバックや Promise のようなコンポーネントの非同期動作をテストする場合、いくつかのテクニックを知っておく必要があります。

よくある非同期動作の 1 つとして API 呼び出しと Vuex の action があります。以下の例は API 呼び出しをするメソッドをテストする方法を示しています。この例は HTTP のライブラリである axios をモックしてテストを実行するために Jest を使っています。Jest のモックの詳細はここにあります。

axios のモックの実装はこのようにします。

export default {
  get: () => Promise.resolve({ data: 'value' })
}

ボタンをクリックすると以下のコンポーネントは API 呼び出しをします。そして、レスポンスを value に代入します。

<template>
  <button @click="fetchResults" />
</template>

<script>
  import axios from 'axios'

  export default {
    data() {
      return {
        value: null
      }
    },

    methods: {
      async fetchResults() {
        const response = await axios.get('mock/service')
        this.value = response.data
      }
    }
  }
</script>

テストはこのように書くことができます。

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios')

it('fetches async when a button is clicked', () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  expect(wrapper.vm.value).toBe('value')
})

fetchResults 内の Promise が resolve する前にアサーションが呼ばれるので、このテストは現時点では失敗します。ほとんどのユニットテストライブラリはテストが完了したことをテストランナーに知らせるためのコールバック関数を提供します。Jest と Mocha は両方とも  done を使います。アサーションが行われる前に確実に各 Promise が resolve するために done$nextTick や  setTimeout と組み合わせて使うことができます。

it('fetches async when a button is clicked', done => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  wrapper.vm.$nextTick(() => {
    expect(wrapper.vm.value).toBe('value')
    done()
  })
})

$nextTick と  setTimeout がテストをパスする理由は $nextTick と  setTimeout を処理するタスクキュー前に Promise のコールバック関数を処理するマイクロタスクキューが実行されるからです。つまり、$nextTick と  setTimeout が実行される前に、マイクロタスクキュー上にあるすべての Promise のコールバック関数が実行されます。より詳しい説明はここを見てください。

もう 1 つの解決策は async function と npm パッケージの flush-promises を使用することです。flush-promises は堰き止められている resolve された Promise ハンドラを流します。堰き止められている Promise を流すこととテストの可読性を改善するために awaitflushPromises の呼び出しの前に置きます。

反映されたテストはこのようになります。

import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

it('fetches async when a button is clicked', async () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  await flushPromises()
  expect(wrapper.vm.value).toBe('value')
})

同じテクニックをデフォルトで Promise を返す Vuex の action に適用することができます。

TypeScript と一緒に使う

この記事のサンプルプロジェクトは、 GitHub にあります。

TypeScript は JavaScript に型とクラスを加えた人気のある JavaScript のスーパーセットです。 Vue Test Utils の型定義は、配布されている Vue Test Utils のパッケージに含まれています。だから、Vue Test Utils と TypeScript はうまく動作します。

ここでは、基本的な Vue CLI を使った TypeScript のセットアップから Jest と Vue Test Utils を使用した TypeScript のテストの作成までを解説します。

TypeScript の追加

最初にプロジェクトを作成します。もし、Vue CLI をインストールしていないなら、 Vue CLI をグローバルにインストールしてください。

$ npm install -g @vue/cli

以下のようにプロジェクトを作成します。

$ vue create hello-world

CLI プロンプトで Manually select features を選択します。そして、 TypeScript を選択して Enter キーを押します。これで TypeScript の設定がされているプロジェクトが生成されます。

注意

Vue と TypeScript を一緒に使うためのセットアップの詳細は、 TypeScript Vue starter guide を確認してください。

次にプロジェクトに Jest を加えます。

Jest のセットアップ

Jest はバッテリー付属のユニットテストソリューションを提供するために Facebook が開発したテストランナです。 Jest の詳細については公式ドキュメント を参照してください。

Jest と Vue Test Utils をインストールします。

$ npm install --save-dev jest @vue/test-utils

次に test:unit スクリプトを package.json に定義します。

// package.json
{
  // ..
  "scripts": {
    // ..
    "test:unit": "jest"
  }
  // ..
}

Jest での単一ファイルコンポーネントの処理

Jest が *.vue ファイルを処理するために vue-jest プリプロセッサをインストールして設定します。

npm install --save-dev vue-jest

次に jest ブロックを package.json に追加します。

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      // `*.vue` ファイルを Jest で取り扱います。
      "vue"
    ],
    "transform": {
      // `vue-jest` で `*.vue` ファイルを処理します。
      ".*\\.(vue)$": "vue-jest"
    },
    "testURL": "http://localhost/"
  }
}

Jest に対応するための TypeScript の設定

テストで TypeScript ファイルを使うために Jest が TypeScript をコンパイルするようにセットアップする必要があります。そのために ts-jest をインストールします。

$ npm install --save-dev ts-jest

次に Jest が TypeScript のテストファイルを ts-jest で処理するために package.jsonjest.transform に設定を追加します。

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // `ts-jest` で `*.ts` ファイルを処理します。
      "^.+\\.tsx?$": "ts-jest"
    }
    // ...
  }
}

テストファイルの配置

デフォルトでは、 Jest はプロジェクトにある拡張子が .spec.js もしくは .test.js のすべてのファイルを対象にします。

拡張子が .ts のテストファイルを実行するために、package.json ファイルの testRegex を変更する必要があります。

以下を package.jsonjest フィールドに追加します。

{
  // ...
  "jest": {
    // ...
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

Jest はテストされるコードと同じディレクトリに __tests__ ディレクトリを作成することを推奨していますが、あなたにとってテストに適したディレクトリ構造にして構いません。ただ、Jest は __snapshots__ ディレクトリをスナップショットテストを実施するテストファイルと同じディレクトリに作成することに注意してください。

ユニットテストを書く

これでプロジェクトのセットアップが完了しました。今度はユニットテストを作成します。

src/components/__tests__/HelloWorld.spec.ts ファイルを作成して、以下のコードを加えます。

// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

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

これが TypeScript と Vue Test Utils を協業させるために必要なことすべてです!

リソース

Vue Router と一緒に使用する

テストへ Vue Router のインストール

テストで Vue のコンストラクタベースの Vue Router をインストールしないでください。Vue Router をインストールすると Vue のプロトタイプの読み取り専用プロパティとして $route$router が追加されます。

これを回避するために、localeVue を作成し、その上に Vue Router をインストールすることができます。

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
  localVue,
  router
})

Vue Router を localVue にインストールすると $route と  $router が読み取り専用プロパティーとして localVue に追加されます。これは VueRouter をインストールした localVue を使用しているコンポーネントをマウントする時、 mock オプションで $route と  $router を上書きすることができないことを意味します。

Vue Router をインストールする時、router-linkrouter-view コンポーネントが登録されます。これは、それらをアプリケーションにインポートする必要がなく、アプリケーションのどこでも使用することができます。

テストを実行する際には、マウントしているコンポーネントにこれら Vue Router のコンポーネントを使用できるようにする必要があります。これらを行うには 2 つの方法があります。

スタブを使用する

import { shallowMount } from '@vue/test-utils'

shallowMount(Component, {
  stubs: ['router-link', 'router-view']
})

localVue による Vue Router のインストール

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)

shallowMount(Component, {
  localVue
})

$route$router のモック

時々、コンポーネントが $route$router オブジェクトから引数によって何かをするテストをしたいときがあります。これをするためには、Vue インスタンスにカスタムモックを渡すことができます。

import { shallowMount } from '@vue/test-utils'

const $route = {
  path: '/some/path'
}

const wrapper = shallowMount(Component, {
  mocks: {
    $route
  }
})

wrapper.vm.$route.path // /some/path

よくある落とし穴

Vue Router をインストールすると Vue のプロトタイプに読み取り専用プロパティとして $route$router が追加されます。

これは、$route または $router をモックを試みるテストが将来失敗することを意味します。

これを回避するために、テストを実行するときに、Vue Router をグローバルにインストールしないでください。
上記のように localVue を使用してください。

Vuex と一緒に使用する

このガイドでは、vue-test-utils でコンポーネントで Vuex をテストする方法について、見ていきます。

コンポーネント内の Vuex をテストする

アクションのモック

それではいくつかのコードを見ていきましょう。

これはテストしたいコンポーネントです。これは Vuex のアクションを呼び出します。

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

このテストの目的のために、アクションが何をしているのか、またはストアがどのように見えるかは気にしません。これらのアクションが必要なときに発行されていること、そして期待された値によって発行されていることを知ることが必要です。

これをテストするためには、私たちのコンポーネントを shallowMount するときに Vue にモックストアを渡す必要があります。

ストアを Vue コンストラクタベースに渡す代わりに、localVue に渡すことができます。localeVue はグローバルな Vue コンストラクタに影響を与えずに、変更を加えることができるスコープ付き Vue コンストラクタです。

これがどのように見えるか見ていきましょう:

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 に localVue.use メソッドを使用して Vuex を使用するように指示しています。これは、単なる Vue.use のラッパです。

次に、新しい Vuex.store をモックした値で呼び出すことによってモックのストアを作成します。それをアクションに渡すだけです。それが気にしなければならないことの全てだからです。

アクションは、Jest のモック関数です。これらモック関数は、アクションが呼び出されたかどうかを検証するメソッドを提供します。

アクションのスタブが期待どおりに呼び出されたことを検証することができます。

今、ストアを定義する方法が、あなたには少し異質に見えるかもしれません。

各テストより前にストアをクリーンに保証するために、beforeEach を使用しています。beforeEach は各テストより前に呼び出される Mocha のフックです。このテストでは、ストア変数に値を再度割り当てています。これをしていない場合は、モック関数は自動的にリセットされる必要があります。また、テストにおいて状態を変更することもできますが、この方法は、後のテストで影響を与えることはないです。

このテストで最も重要なことは、モック Vuex ストアを作成し、それを vue-test-utils に渡す ことです。

素晴らしい!今、アクションをモック化できるので、ゲッタのモックについて見ていきましょう。

ゲッタのモック

<template>
  <div>
    <p v-if="inputValue">{{inputValue}}</p>
    <p v-if="clicks">{{clicks}}</p>
  </div>
</template>

<script>
  import { mapGetters } from 'vuex'

  export default {
    computed: mapGetters(['clicks', 'inputValue'])
  }
</script>

これは、かなり単純なコンポーネントです。ゲッタによる clicks の結果と inputValue を描画します。また、これらゲッタが返す値については実際に気にしません。それらの結果が正しく描画されているかだけです。

テストを見てみましょう:

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

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Getters.vue', () => {
  let getters
  let store

  beforeEach(() => {
    getters = {
      clicks: () => 2,
      inputValue: () => 'input'
    }

    store = new Vuex.Store({
      getters
    })
  })

  it('Renders state.inputValue in first p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(getters.inputValue())
  })

  it('Renders state.clicks in second p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.findAll('p').at(1)
    expect(p.text()).toBe(getters.clicks().toString())
  })
})

このテストはアクションのテストに似ています。各テストの前にモックストアを作成し、shallowMount を呼び出すときにオプションを渡し、そしてモックゲッタから返された値を描画されているのを検証します。

これは素晴らしいですが、もしゲッタが状態の正しい部分を返しているのを確認したい場合はどうしますか?

モジュールによるモック

モジュールはストアを管理しやすい塊に分けるために便利です。それらはゲッタもエクスポートします。テストではこれらを使用することができます。

コンポーネントを見てみましょう:

<template>
  <div>
    <button @click="moduleActionClick()">Click</button>
    <p>{{moduleClicks}}</p>
  </div>
</template>

<script>
  import { mapActions, mapGetters } from 'vuex'

  export default {
    methods: {
      ...mapActions(['moduleActionClick'])
    },

    computed: mapGetters(['moduleClicks'])
  }
</script>

1 つのアクションと 1 つのゲッタを含む単純なコンポーネントです。

そしてテストは以下のようになります:

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

const localVue = createLocalVue()

localVue.use(Vuex)

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

  beforeEach(() => {
    state = {
      clicks: 2
    }

    actions = {
      moduleActionClick: jest.fn()
    }

    store = new Vuex.Store({
      modules: {
        myModule: {
          state,
          actions,
          getters: myModule.getters
        }
      }
    })
  })

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

  it('renders "state.inputValue" in first p tag', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(state.clicks.toString())
  })
})

Vuex ストアのテスト

Vuex ストアをテストする方法が 2 つあります。1 つ目はゲッタとミューテーションとアクションを別々に単体テストする方法です。2 つ目はストアを生成してそれをテストする方法です。

Vuex ストアをテストする方法を説明するためにシンプルなカウンターストアを用意します。このストアには increment ミューテーションと evenOrOdd ゲッタがあります。

// mutations.js
export default {
  increment(state) {
    state.count++
  }
}
// getters.js
export default {
  evenOrOdd: state => (state.count % 2 === 0 ? 'even' : 'odd')
}

ゲッタとミューテーションとアクションを別々にテストする

ゲッタとミューテーションとアクションはすべて JavaScript の関数です。それらは vue-test-utils と Vuex を使用しなくてもテストすることができます。

ゲッタとミューテーションとアクションを別々にテストする利点は単体テストを詳細に記述することができることです。テストが失敗すると、コードの何が原因か正確に知ることができます。欠点は commit や  dispatch のような Vuex の関数のモックが必要なことです。これは不正なモックが原因で単体テストはパスしてプロダクションは失敗する状況を作り出す可能性があります。

mutations.spec.js と getters.spec.js という名前のテストファイルを 2 つ作成します。

最初に increment ミューテーションをテストします。

// mutations.spec.js

import mutations from './mutations'

test('increment increments state.count by 1', () => {
  const state = {
    count: 0
  }
  mutations.increment(state)
  expect(state.count).toBe(1)
})

今度は evenOrOdd ゲッタを次の手順でテストします。 state モックを作成します。 state を引数としてゲッタ関数を実行します。そして、それが正しい値を返したか確認します。

// getters.spec.js

import getters from './getters'

test('evenOrOdd returns even if state.count is even', () => {
  const state = {
    count: 2
  }
  expect(getters.evenOrOdd(state)).toBe('even')
})

test('evenOrOdd returns odd if state.count is odd', () => {
  const state = {
    count: 1
  }
  expect(getters.evenOrOdd(state)).toBe('odd')
})

実行可能なストアのテスト

Vuex ストアをテストするもう 1 つの方法はストアの設定を使って実行可能なストアを生成することです。

実行可能なストアを生成してテストすることの利点は Vuex の関数をモックする必要がない事です。

欠点はテストが失敗した時、問題がある箇所を見つけることが難しいことです。

テストを書いてみましょう。ストアを生成する際は、 Vue のコンストラクタが汚染されることを避けるために localVue を使用します。このテストは store-config.js の export を使用してストアを生成します。

// store-config.js

import mutations from './mutations'
import getters from './getters'

export default {
  state: {
    count: 0
  },
  mutations,
  getters
}
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'

test('increments count value when increment is committed', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.state.count).toBe(0)
  store.commit('increment')
  expect(store.state.count).toBe(1)
})

test('updates evenOrOdd getter when increment is committed', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.getters.evenOrOdd).toBe('even')
  store.commit('increment')
  expect(store.getters.evenOrOdd).toBe('odd')
})

ストアをストアの設定から生成する前に cloneDeep を使用しています。こうする理由は Vuex はストアを生成するためにオプションオブジェクトを変更するからです。どのテストでも確実に汚染されていないストアを使うために storeConfig オブジェクトを複製する必要があります。

リソース