こんにちは、かつコーチです。
前回は、Vitestの基本と、純粋な関数に対するテストの書き方を解説しました。
今回は、Vue公式のテストユーティリティであるVue Test Utilsを使って、Vueコンポーネントそのものをテストする方法を扱います。
Vue Test Utilsとは?
コンポーネントを仮想的にマウントするライブラリ
Vue Test Utilsは、Vueコンポーネントをテスト環境上に仮想的に描画(マウント)し、その結果を検証するための公式ライブラリです。
実際のブラウザを起動しなくても、「このpropsを渡したら、この文字列が表示される」「このボタンをクリックしたら、このイベントが発火する」といった振る舞いを確認できます。
前回インストールした@vue/test-utilsが、このためのパッケージです。
mountとshallowMountの違い
Vue Test Utilsには、コンポーネントを描画するための関数としてmountとshallowMountが用意されています。
| 関数 | 子コンポーネント |
|---|---|
mount | 子コンポーネントも含めて実際に描画する |
shallowMount | 子コンポーネントはスタブ(簡易的な代替表示)に置き換える |
子コンポーネントの中身まで検証したい場合はmountを、対象のコンポーネント自体のロジックだけに集中したい場合はshallowMountを使う、という使い分けが基本です。
迷ったら、まずはmountで書き始めて問題ない場合がほとんどです。
基本のコンポーネントテスト
対象のコンポーネントを用意する
シンプルなカウンターコンポーネントを例にします。
<!-- Counter.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
<template>
<div>
<p data-testid="count">{{ count }}</p>
<button @click="increment">増やす</button>
</div>
</template>
data-testid属性は、テストからその要素を確実に特定するために付与しています。
CSSクラス名で要素を取得するとデザイン変更のたびにテストが壊れやすいため、テスト専用の目印を用意しておくのがおすすめです。
テストコードを書く
// Counter.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
it('初期表示では0が表示される', () => {
const wrapper = mount(Counter)
expect(wrapper.get('[data-testid="count"]').text()).toBe('0')
})
it('ボタンをクリックすると数値が1増える', async () => {
const wrapper = mount(Counter)
await wrapper.get('button').trigger('click')
expect(wrapper.get('[data-testid="count"]').text()).toBe('1')
})
})
mount(Counter)でコンポーネントを描画し、wrapper.get()で要素を取得します。
trigger('click')でクリックイベントを発火させ、その結果として表示が変わったかどうかをexpectで検証しています。
propsとイベントのテスト
propsを渡してテストする
親から受け取ったpropsに応じて表示が変わるコンポーネントも、同じ考え方でテストできます。
<!-- UserBadge.vue -->
<script setup lang="ts">
interface Props {
name: string
isOnline: boolean
}
defineProps<Props>()
</script>
<template>
<div>
<span>{{ name }}</span>
<span :data-testid="'status'">{{ isOnline ? 'オンライン' : 'オフライン' }}</span>
</div>
</template>
// UserBadge.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import UserBadge from './UserBadge.vue'
describe('UserBadge', () => {
it('オンラインのときは「オンライン」と表示される', () => {
const wrapper = mount(UserBadge, {
props: { name: 'かつコーチ', isOnline: true },
})
expect(wrapper.get('[data-testid="status"]').text()).toBe('オンライン')
})
})
mountの第2引数にpropsを渡すことで、propsを指定した状態でコンポーネントを描画できます。
emitされたイベントを検証する
子コンポーネントが発火するカスタムイベントも検証できます。
describe('UserBadge', () => {
it('クリックするとselectイベントが発火する', async () => {
const wrapper = mount(UserBadge, {
props: { name: 'かつコーチ', isOnline: true },
})
await wrapper.trigger('click')
expect(wrapper.emitted('select')).toBeTruthy()
})
})
wrapper.emitted('イベント名')で、そのイベントが実際に発火したかどうかや、渡された引数を確認できます。
つまずきやすいポイント:非同期の描画更新
私が実際にハマった「クリックしたのに表示が変わらない」問題
Vue Test Utilsを使い始めた頃、ボタンをクリックしてもexpectの結果が更新前の値のままになる、という現象に何度もつまずきました。
❌ Before:triggerのawaitを忘れてしまう
it('ボタンをクリックすると数値が1増える', () => {
const wrapper = mount(Counter)
wrapper.get('button').trigger('click') // awaitを忘れている
expect(wrapper.get('[data-testid="count"]').text()).toBe('1')
})
Vueの画面更新(DOMの再描画)は、内部的に非同期で行われます。
trigger('click')自体もPromiseを返す関数なので、awaitをつけないと、DOMの更新が終わる前にexpectが実行されてしまい、まだ古い表示のまま判定されてしまいます。
私はこの現象に最初出会ったとき、「クリックのロジック自体がおかしいのでは」とコンポーネント側のコードを疑ってしまい、原因の特定に無駄な時間をかけてしまいました。
✅ After:triggerに必ずawaitをつける
it('ボタンをクリックすると数値が1増える', async () => {
const wrapper = mount(Counter)
await wrapper.get('button').trigger('click')
expect(wrapper.get('[data-testid="count"]').text()).toBe('1')
})
triggerだけでなく、setPropsなど画面の再描画を伴う操作は基本的にawaitが必要になると覚えておきましょう。
「クリック系のメソッドには基本awaitをつける」というルールにしておくと、こうした事故を減らせます。
まとめ
この記事のポイント
- Vue Test Utilsを使うと、ブラウザを起動せずにVueコンポーネントの描画結果を検証できる
mountは子コンポーネントも実際に描画し、shallowMountは子コンポーネントをスタブに置き換えるdata-testidのようなテスト専用の目印を用意すると、デザイン変更に強いテストになる- propsは
mountの第2引数で渡し、emitted()で発火したイベントを検証できる triggerなど画面更新を伴う操作にはawaitが必要で、忘れると更新前の表示のまま判定されてしまう
次に読むべき記事
次回は「コンポーネントのユニットテスト実践」を解説します。
→ 次の記事:コンポーネントのユニットテスト実践