404 Not Found

404 Not Found


nginx

Vue 3 のユニットテストとE2Eテスト:Vitest + Vue Test Utils + Playwright

ユニットテストは、コードの品質を確保するための基盤です。これにより、不具合を引き起こす心配なくコンポーネントのリファクタリングが可能になり、CIによるテストの自動実行が可能になり、新しいチームメンバーも自信を持ってコードに変更を加えられるようになります。Vue 3では、Vitest(Jestよりも10倍高速な次世代テストフレームワーク)とVue Test Utils(公式のコンポーネントテストツール)の使用が推奨されています。

E2Eテストは実際のユーザーの操作をシミュレートします。Playwrightは現在、最も人気のあるE2Eテストツールです(Cypressよりも高速で、クロスブラウザ対応)。このコースでは、ユニットテストとE2Eテストの両方を網羅した包括的なテストフレームワークについて解説します。

1. 学習内容


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) 収益

テストを追加した後:


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つの間違いのクイックリファレンス

エラー 症状 解決策
コンポーネントが見つかりません インポートエラー パスエイリアスを確認してください
待機なしの非同期テスト 常に失敗する awaitflushPromises を使用
トリガーイベントが機能しない リスナーがトリガーされない 「trigger」の後に「await」がある
E2E タイムアウト 要素が見つかりません waitForSelector
カバレッジが低い テストが記述されていない 必須のカバレッジ閾値

(5) ▶ サンプル:. 主要な5つのテストツールの比較

工具 回転数 用途
Vitest ⭐⭐⭐⭐⭐ 単体テスト
Vue Test Utils ⭐⭐⭐⭐⭐ コンポーネントのテスト
劇作家 ⭐⭐⭐⭐ E2Eテスト
サイプレス ⭐⭐⭐ E2E (低速)
テストライブラリ ⭐⭐⭐⭐ コンポーネントテスト

❓ よくある質問

Q VitestとJest、どちらを選べばいいですか?
A Vitestです。新規プロジェクトにはすべてVitestを使用してください(10倍高速で、ESM/TSをネイティブでサポートしています)。レガシープロジェクトのメンテナンスにはJestを使用してください。
Q mountshallowMount、どちらを選べばいいですか?
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ステップを追加してください。テストに失敗した場合は、プルリクエストのマージをブロックしてください。

📖 まとめ


📝 練習問題

(1) 基礎問題(難易度:⭐)

ProductCard に対して 3 つのテストを作成してください:

(2) 上級問題(難易度:⭐⭐)

store ショッピングカートに対して包括的なテストを作成してください:

(3) チャレンジ問題(難易度:⭐⭐⭐)

包括的な「テストフレームワーク」を実装する:

  1. コンポーネント向けのユニットテスト10件(コンポーネントあたり3~5件)
  2. 5つの組み合わせ可能なテスト
  3. エンドツーエンドテスト 5件(ログイン/登録/検索/ショッピングカート/決済)
  4. 補償対象の閾値:80%
  5. CIの統合(GitHub Actions)
  6. Vitest と Playwright を使ったクロスブラウザテスト
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%