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)
})
})
次は何をするのか
- テストランナを選ぶで
vue-test-utils
をプロジェクトに組み込む - テストを書くときの一般的なヒントについてもっと知る
一般的なヒント
何をテストするかを知る
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({})
メソッドを使用して、すでにマウントされているコンポーネントのプロパティを更新することもできます。
オプションの完全なリストについては、ドキュメントのマウントオプションのセクションを参照してください。
グローバルプラグインとミックスインの適用
コンポーネントの中には、グローバルプラグインやミックスインによって注入された機能 (例: vuex
、vue-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 イベントのテスト
イベントをトリガする
Wrapper
の trigger
メソッドで 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 を流すこととテストの可読性を改善するために await
を flushPromises
の呼び出しの前に置きます。
反映されたテストはこのようになります。
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.json
の jest.transform
に設定を追加します。
{
// ...
"jest": {
// ...
"transform": {
// ...
// `ts-jest` で `*.ts` ファイルを処理します。
"^.+\\.tsx?$": "ts-jest"
}
// ...
}
}
テストファイルの配置
デフォルトでは、 Jest はプロジェクトにある拡張子が .spec.js
もしくは .test.js
のすべてのファイルを対象にします。
拡張子が .ts
のテストファイルを実行するために、package.json
ファイルの testRegex
を変更する必要があります。
以下を package.json
の jest
フィールドに追加します。
{
// ...
"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
を上書きすることができないことを意味します。
router-link
または router-view
を使用するコンポーネントテスト
Vue Router をインストールする時、router-link
と router-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
オブジェクトを複製する必要があります。