Vue 3 のユニットテストとE2Eテスト:Vitest + Vue Test Utils + Playwright
ユニットテストは、コードの品質を確保するための基盤です。これにより、不具合を引き起こす心配なくコンポーネントのリファクタリングが可能になり、CIによるテストの自動実行が可能になり、新しいチームメンバーも自信を持ってコードに変更を加えられるようになります。Vue 3では、Vitest(Jestよりも10倍高速な次世代テストフレームワーク)とVue Test Utils(公式のコンポーネントテストツール)の使用が推奨されています。
E2Eテストは実際のユーザーの操作をシミュレートします。Playwrightは現在、最も人気のあるE2Eテストツールです(Cypressよりも高速で、クロスブラウザ対応)。このコースでは、ユニットテストとE2Eテストの両方を網羅した包括的なテストフレームワークについて解説します。
1. 学習内容
- Vitestの設定と4つのコアAPI
- Vue Test Utils (@vue/test-utils) のコンポーネントのマウント
- 5つの主要なコンポーネントテストシナリオ(props / emit / slot / event / async)
- 5つの重要な主張のテクニック
- 劇作家のE2Eテスト
- 取材レポート
- よくある5つの間違い
2. 「リファクタリング恐怖症」に悩むチームの課題
(1) 課題:1つのコンポーネントの設定を誤って変更すると、サイト全体がクラッシュしてしまう
アリスのチームは、100以上のコンポーネントからなるEC管理システムを運用していました:
JS
// ❌ The "Flip" Version:Not tested,Alice I don't dare to refactor
// ProductCard.vue Changed 1 line, The entire home page is a blank screen
// No one noticed, Day 2 not until a user filed a complaint they discovered
// Team:"Don't move this component,We don't know what it will destroy."
プロダクトマネージャーのチャーリー:
「アリス、テストが必要なんだ。不安なくリファクタリングできるようにしたい。必要なのは、1) コンポーネントのユニットテスト、2) 重要なフローに対するE2Eテストだ。」
(2) Vitest + Vue テストユーティリティソリューション
JS
// tests/unit/ProductCard.test.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ProductCard from '@/components/ProductCard.vue'
describe('ProductCard', () => {
it('renders product name and price', () => {
const product = { id: 1, name: 'iPhone', price: 999 }
const wrapper = mount(ProductCard, { props: { product } })
expect(wrapper.text()).toContain('iPhone')
expect(wrapper.text()).toContain('$999')
})
it('emits add-to-cart event', async () => {
const wrapper = mount(ProductCard, {
props: { product: { id: 1, name: 'iPhone', price: 999 } }
})
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('add-to-cart')).toBeTruthy()
expect(wrapper.emitted('add-to-cart')[0]).toEqual([1])
})
})
テストを実行します:
BASH
$ npx vitest run
✓ ProductCard > renders product name and price
✓ ProductCard > emits add-to-cart event
2 passed
5秒で実行され、誤検知は0件。コードを変更した後、すぐに動作に不具合があるかどうかがわかります。
(3) 収益
テストを追加した後:
- 自信を取り戻す:100%の改善(悪化させる心配がない)
- バグの早期発見:80%は開発段階で発見される
- ドキュメントの目的:このテストは、当該コンポーネントのユーザーガイドとしての役割を果たします。
- CIの自動化:プルリクエストに対してテストを自動的に実行する
3. Vitestの設定
(1) インストール
BASH
npm install -D vitest @vue/test-utils @vitest/coverage-v8 jsdom
(2) 基本設定
TS
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
test: {
// Test Environment
environment: 'jsdom', // or 'happy-dom' / 'node'
// Matching Files
include: ['tests//*.test.ts', 'src//*.test.ts'],
// Coverage Rate
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json'],
exclude: ['node_modules/', 'tests/']
},
// Global setup
setupFiles: ['./tests/setup.ts']
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
(3) 4つの主要なコアAPI
TS
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// 1. describe:Test Suite
describe('Calculator', () => { })
// 2. it:Single Test
it('adds 1 + 1 to equal 2', () => { })
// 3. expect:Assertion
expect(1 + 1).toBe(2)
expect(arr).toContain('apple')
expect(obj).toMatchObject({ name: 'Alice' })
// 4. vi:mock / spy
vi.fn() // mock Function
vi.mock('./api') // mock Module
vi.spyOn(obj, 'method') // spy Methods
(4) Vitest 対 Jest
| 次元 | かどうか | Vitest |
|---|---|---|
| 起動時間 | 3~5 秒 | < 1 秒 |
| リスニングモード | 2~3秒 | < 100ミリ秒 |
| ESM サポート | 設定が必要 | ネイティブ |
| TypeScript | ts-jest が必要 | ネイティブ |
| 互換性 | 広範 | Jest 互換 API |
| 評価 | 古い | ⭐⭐⭐⭐⭐ |
4. Vue Test Utils コンポーネントのテスト
(1) 5つの主要なAPI
JS
import { mount, shallowMount, RouterLinkStub } from '@vue/test-utils'
// 1. mount:Full Mount(Child components are also mounted)
const wrapper = mount(Component, { props: { ... } })
// 2. shallowMount: Shallow Mount (Child components are stubbed)
const wrapper = shallowMount(Component, { props: { ... } })
// 3. Find an Element
wrapper.find('button') // First match
wrapper.findAll('li') // All matches
wrapper.findComponent(MyComponent) // Find Child Components
wrapper.get('#submit') // Must be found(Error not found)
// 4. Trigger Event
await wrapper.find('button').trigger('click')
await wrapper.find('input').setValue('hello')
// 5. Assertion
expect(wrapper.text()).toContain('Hello')
expect(wrapper.find('h1').text()).toBe('Title')
expect(wrapper.emitted('add-to-cart')).toBeTruthy()
(2) 5つの主要なテストシナリオ
JS
// 1. props Test
it('renders props correctly', () => {
const wrapper = mount(MyComponent, { props: { title: 'Hello' } })
expect(wrapper.text()).toContain('Hello')
})
// 2. emit Test
it('emits event on click', async () => {
const wrapper = mount(MyComponent)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('submit')).toBeTruthy()
})
// 3. slot Test
it('renders default slot', () => {
const wrapper = mount(MyComponent, {
slots: { default: '<p>Slot content</p>' }
})
expect(wrapper.html()).toContain('Slot content')
})
// 4. Asynchronous Testing
it('loads data on mount', async () => {
const wrapper = mount(MyComponent)
await flushPromises() // Wait for asynchronous completion
expect(wrapper.text()).toContain('Loaded')
})
// 5. provide/inject Test
it('uses provided value', () => {
const wrapper = mount(MyComponent, {
provide: { theme: 'dark' }
})
expect(wrapper.find('.dark').exists()).toBe(true)
})
(3) 主張を効果的に伝える5つのテクニック
JS
// 1. The text contains
expect(wrapper.text()).toContain('Hello')
// 2. class Existence
expect(wrapper.find('.active').exists()).toBe(true)
// 3. Elemental Presence
expect(wrapper.find('button').exists()).toBe(true)
// 4. Event Trigger
expect(wrapper.emitted('click')).toBeTruthy()
expect(wrapper.emitted('click')[0]).toEqual([arg1, arg2])
// 5. Asynchronous operation completed
await wrapper.vm.$nextTick()
await flushPromises()
5. Playwrightによるエンドツーエンドテスト
(1) インストール
BASH
npm install -D @playwright/test
npx playwright install
(2) 設定
TS
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry'
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } }
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI
}
})
(3) 5つの主要なE2Eテストシナリオ
TS
// 1. Login Process
import { test, expect } from '@playwright/test'
test('user can login', async ({ page }) => {
await page.goto('/login')
await page.fill('input[name="username"]', 'alice@example.com')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('Welcome, Alice')
})
// 2. Add to Cart
test('add product to cart', async ({ page }) => {
await page.goto('/products/1')
await page.click('button:has-text("Add to Cart")')
await expect(page.locator('.cart-count')).toHaveText('1')
})
// 3. Form Submission
test('submit form with validation', async ({ page }) => {
await page.goto('/contact')
await page.fill('input[name="email"]', 'invalid-email')
await page.click('button[type="submit"]')
await expect(page.locator('.error')).toBeVisible()
})
// 4. Route Redirection
test('navigate to products page', async ({ page }) => {
await page.goto('/')
await page.click('a:has-text("Products")')
await expect(page).toHaveURL('/products')
})
// 5. Asynchronous Loading
test('loads data after navigation', async ({ page }) => {
await page.goto('/dashboard')
// Waiting for data to load
await page.waitForSelector('.chart-loaded')
await expect(page.locator('.chart-loaded')).toBeVisible()
})
(4) E2Eテストを実行する
BASH
# Run all tests
npx playwright test
# Run a Specific File
npx playwright test tests/e2e/login.spec.ts
# Debug Mode(Open your browser)
npx playwright test --debug
# UI Pattern(Visualization)
npx playwright test --ui
6. 完全な例:5つの主要なテストシナリオ
(1) ▶ サンプル:. 5つの主要コンポーネントのテスト
JS
// 1. props
it('renders props', () => { ... })
// 2. emit
it('emits event', async () => { ... })
// 3. slot
it('renders slot', () => { ... })
// 4. async
it('loads data', async () => { ... })
// 5. provide
it('uses provide', () => { ... })
(2) ▶ サンプル:. 5つの主要な主張のテクニック
JS
// 1. Text
expect(wrapper.text()).toContain('Hello')
// 2. class
expect(wrapper.find('.active').exists()).toBe(true)
// 3. Element
expect(wrapper.find('button').exists()).toBe(true)
// 4. Event
expect(wrapper.emitted('click')).toBeTruthy()
// 5. Asynchronous
await wrapper.vm.$nextTick()
(3) ▶ サンプル:. 5つの主要なE2Eシナリオ
TS
// 1. Log In
test('login', async ({ page }) => { ... })
// 2. Shopping Cart
test('add to cart', async ({ page }) => { ... })
// 3. Form
test('form submit', async ({ page }) => { ... })
// 4. Routing
test('navigate', async ({ page }) => { ... })
// 5. Asynchronous
test('async load', async ({ page }) => { ... })
(4) ▶ サンプル:. よくある5つの間違いのクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| コンポーネントが見つかりません | インポートエラー | パスエイリアスを確認してください |
| 待機なしの非同期テスト | 常に失敗する | await + flushPromises を使用 |
| トリガーイベントが機能しない | リスナーがトリガーされない | 「trigger」の後に「await」がある |
| E2E タイムアウト | 要素が見つかりません | waitForSelector |
| カバレッジが低い | テストが記述されていない | 必須のカバレッジ閾値 |
(5) ▶ サンプル:. 主要な5つのテストツールの比較
| 工具 | 回転数 | 用途 |
|---|---|---|
| Vitest | ⭐⭐⭐⭐⭐ | 単体テスト |
| Vue Test Utils | ⭐⭐⭐⭐⭐ | コンポーネントのテスト |
| 劇作家 | ⭐⭐⭐⭐ | E2Eテスト |
| サイプレス | ⭐⭐⭐ | E2E (低速) |
| テストライブラリ | ⭐⭐⭐⭐ | コンポーネントテスト |
❓ よくある質問
Q VitestとJest、どちらを選べばいいですか?
A Vitestです。新規プロジェクトにはすべてVitestを使用してください(10倍高速で、ESM/TSをネイティブでサポートしています)。レガシープロジェクトのメンテナンスにはJestを使用してください。
Q
mount と shallowMount、どちらを選べばいいですか?A ユニットテストには
shallowMount を使用してください(処理が速く、隔離性が高い)。統合テストには mount を使用してください(より現実的です)。推奨:コンポーネントテストには shallowMount を、いくつかの主要なコンポーネントには mount を使用してください。Q 適切なテストカバレッジ率はどれくらいですか?
A コアコンポーネントは80%以上、ユーティリティは90%以上、全体では60%以上です。100%を目指す必要はありません(現実的ではありません)。
Q PlaywrightとCypress、どちらを選ぶべきですか?
A Playwrightです。処理速度がほぼ2倍で、複数のブラウザ(Chromium、Firefox、WebKit)に対応しており、APIも優れています。CypressはChromiumベースのブラウザにしか対応していません。
Q ユニットテストとE2Eテストの割合はどのようになっていますか?
A 70%がユニットテスト、20%が統合テスト、10%がE2Eテストです。E2Eテストは実行に時間がかかり、不安定ですが、ユニットテストはコストパフォーマンスが最も優れています。
Q テスト中はすべての依存関係をモック化すべきですか?
A ユニットテストでは、外部API(fetch、localStorageなど)をモック化してください。統合テストでは、実際の依存関係を使用してください。E2Eテストでは、すべてをそのまま使用してください。
Q CIでテストを実行するにはどうすればよいですか?
A GitHub ActionsまたはGitLab CIに
npm testステップを追加してください。テストに失敗した場合は、プルリクエストのマージをブロックしてください。📖 まとめ
- Vitest は Vue 3 に推奨されるテストフレームワークです(Jest より 10 倍高速です)
- Vue Test Utils:mount / shallowMount / 5つの主要API
- 5つのコンポーネントテストの種類:props / emit / slot / async / provide
- 5つのアサーションの種類:text / class / element / event / async
- Playwright は、推奨されるエンドツーエンド(E2E)テストツールです(高速/クロスブラウザ対応)。
- テストカバレッジ:コア 80%以上、全体 60%以上
- 70%が単体テスト + 20%が統合テスト + 10%がエンドツーエンドテスト
📝 練習問題
(1) 基礎問題(難易度:⭐)
ProductCard に対して 3 つのテストを作成してください:
- プロップスのレンダリング
- イベントを発行する
- 境界ケース(在庫が0の場合)
(2) 上級問題(難易度:⭐⭐)
store ショッピングカートに対して包括的なテストを作成してください:
- 5つのアクション(addItem / removeItem / clear / load / save)
- 3つのゲッター(itemCount / totalPrice / isEmpty)
- ピニア試験のセットアップ
(3) チャレンジ問題(難易度:⭐⭐⭐)
包括的な「テストフレームワーク」を実装する:
- コンポーネント向けのユニットテスト10件(コンポーネントあたり3~5件)
- 5つの組み合わせ可能なテスト
- エンドツーエンドテスト 5件(ログイン/登録/検索/ショッピングカート/決済)
- 補償対象の閾値:80%
- CIの統合(GitHub Actions)
- Vitest と Playwright を使ったクロスブラウザテスト



