言語を切り替える
テーマを切り替える

Vitest コンポーネントテスト実践:Browser Mode と Playwright 連携

正直に言うと、初めて Canvas コンポーネントをテストした時、大きな落とし穴にはまりました。

ある日の深夜 2 時、テストレポートに表示された緑色の「PASS」を見て、自信満々でコードをコミットしました。翌日、同僚がリアルブラウザでページを開くと——Canvas がまったくレンダリングされていませんでした。テストは通っているのに?!

後になってわかったことですが、jsdom は「偽の」ブラウザに過ぎません。DOM API をシミュレートしていますが、Canvas の実際のレンダリング動作、CSS 計算スタイル、Web Components のライフサイクル——これらはすべてテストできないのです。半年間使っていたユニットテスト設定は、表面的なことしかテストしていなかったのです。

これが、Vitest がバージョン 3.0 で Browser Mode を導入した理由です——リアルブラウザで直接テストを実行します。jsdom のように Node.js 内で DOM をシミュレートするのではなく、Browser Mode は Chromium/Firefox/Safari を起動し、コンポーネントを実際にレンダリングして、Playwright の API でインタラクションします。テストが通った?なら、本当に通ったのです。

この記事では、Browser Mode の初期設定から、React/Vue コンポーネントの実践、さらに CI 環境でのカバレッジゲートまで解説します。これは Vitest テストガイドシリーズの第 3 弾です。前の 2 回はユニットテストの設定と TDD フローについて触れましたが、今回はコンポーネントテストというパズルのピースを埋めます。

なぜ Browser Mode が必要なのか?

「jsdom では不十分なのか?」と思うかもしれません。

正直に言うと、純粋なロジックコンポーネント——例えば電卓やフォームバリデーション——には、jsdom で十分です。Node.js 環境内の DOM シミュレーターで、速度は高速、設定もシンプルです。前の 2 回の記事で説明したユニットテストは、すべて jsdom 環境で実行され、リアクティブデータやイベントトリガーのテストには問題ありませんでした。

しかし、以下のシナリオでは、jsdom では対応できません:

  • Canvas 描画:jsdom には Canvas API がありますが、実際に描画はしません。ctx.fillRect() が何回呼ばれたかをテストするのは問題ありませんが、描画された図形が正しいかどうかはテストできません。
  • CSS 計算スタイルgetComputedStyle() は jsdom で空のオブジェクトを返します。リアルブラウザでは、要素の幅は親コンテナ、padding、border の影響を受けます——jsdom では計算できません。
  • Web Components:カスタムエレメントの connectedCallbackdisconnectedCallback は jsdom でシミュレートされていますが、ライフサイクルのトリガータイミングはリアルブラウザと異なります。
  • 非同期レンダリング:アニメーションフレーム、requestIdleCallback、IntersectionObserver——これらの API は jsdom で実装されていないか、不完全な実装です。

私が経験した落とし穴を紹介します。去年のプロジェクトで、CSS アニメーションでボタンの展開/収納を制御するコンポーネントがありました。jsdom テストでは、transitionend イベントは決してトリガーされません——実際の transition がないからです。テストはモックイベントで記述されましたが、リアルブラウザではアニメーション時間が変更され、テストはまだ「通過」していましたが、コンポーネントは既に壊れていました。

Browser Mode は、まさにこれらの問題を解決するために存在します。リアルブラウザでコンポーネントをレンダングし、テストコードを書くと、Chromium(または Firefox/Safari)を起動し、コンポーネントをページにマウントし、Playwright の API でクリック、入力、待機を行います。リアルブラウザで何が起こるか、テストでそれをテストします。

興味深いデータがあります。Vitest 3.0 の Browser Mode は Chromium コンテキストを共有し、テスト起動時にブラウザを 1 回だけ開き、すべてのテストが同じインスタンスを共有します。公式によると、従来の Playwright E2E より 30% 高速です。50 個のコンポーネントをテストしても、ブラウザを繰り返し起動・終了する必要はありません。

もう一点、テストピラミッドについては議論があります。従来のピラミッドでは、ユニットテストが大部分を占め、E2E が最も少ないとされます。しかし、Vue 公式ブログには逆ピラミッドの観点があります——alexop.dev のテストピラミッド反転:統合テスト 70%、ユニットテスト 20%、E2E 10%。理由は、コンポーネント自体が統合単位であり、テンプレート、スタイル、ロジックを組み合わせているため、jsdom でのユニットテストはロジック部分しかテストできず、統合テストで初めて全体の動作を検証できるからです。Browser Mode は、この空白を埋めるものです——jsdom よりリアルで、Playwright E2E より軽量です。

Browser Mode 設定の実践

Browser Mode の設定は、実はそれほど複雑ではありません。しかし、私が経験したいくつかの落とし穴を事前に紹介しておきます。

依存関係のインストール

まず、Vitest と Browser Mode プロバイダーをインストールします。公式は Playwright を推奨しています:

npm install -D vitest @vitest/browser-playwright

Playwright は Chromium、Firefox、WebKit の 3 つのブラウザをインストールします。Chromium だけをテストしたい場合(ほとんどの場合、これで十分)、インストール時に指定できます:

npx playwright install chromium

この手順は少し時間がかかります。Chromium パッケージは約 170MB です。ダウンロードが完了すれば、ほぼ準備完了です。

vitest.config.ts の設定

設定ファイルはシンプルですが、注意点があります:

import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'

export default defineConfig({
  test: {
    browser: {
      provider: playwright(),
      enabled: true,
      instances: [{ browser: 'chromium' }],
    },
  },
})

ここで instances 設定は、どのブラウザでテストを実行するかを決定します。複数のブラウザで互換性をテストしたい場合、Firefox と WebKit を追加できます:

instances: [
  { browser: 'chromium' },
  { browser: 'firefox' },
  { browser: 'webkit' },  // Safari
]

ただし、私は通常 Chromium だけを有効にしています——複数ブラウザテストは遅く、フロントエンドのバグの大部分は Chromium で検出できるからです。Safari の互換性問題は、Playwright E2E で重要なパスをカバーしています。

Headless モード vs UI モード

この選択は少し微妙です。

  • Headless モード:ブラウザはウィンドウを開かず、バックグラウンドでテストを実行します。CI 環境に適しており、速度は高速ですが、コンポーネントのレンダリングプロセスは見えません。
  • UI モード:ブラウザがウィンドウを開き、コンポーネントがレンダリング、クリック、入力される様子を確認できます。開発段階でのデバッグに適しており、テストを書く際に便利です。

私は開発時に UI モードを使用し、次のコマンドを実行します:

npx vitest --browser.ui

Vitest はテストワークベンチを開き、左側にテストリスト、右側にブラウザウィンドウが表示されます。テストファイルをクリックすると、ブラウザにコンポーネントがレンダリングされ、テストコードの実行プロセスが見えます。ボタンクリックに応答がない?ブラウザで直接デバッグできます。

CI 環境では Headless モードを使用し、設定に 1 行追加します:

browser: {
  provider: playwright(),
  enabled: true,
  headless: true,  // CI で headless を強制
  instances: [{ browser: 'chromium' }],
}

テストファイルの命名規則

公式は Browser Mode テストファイルに .browser.test.ts という命名を推奨し、通常の .test.ts と区別しています。試してみたところ、次の利点があります:

  • jsdom ユニットテストと Browser Mode コンポーネントテストを同時に実行でき、混在しません。
  • CI で予期せず失敗した場合、ファイル名を見ればブラウザテストだとわかり、調査の方向性が明確です。

ただし、Vitest はこの命名を強制しません。.test.ts を使用しても問題ありません。重要なのは、設定で Browser Mode テストを含むディレクトリを指定するか、すべてのテストを Browser Mode で実行することです。私は分けることを好み、ユニットテストは jsdom、コンポーネントテストは Browser Mode を使用しています。

React/Vue コンポーネントテストの実践

ここが Browser Mode の核心的な使い方です。正直に言うと、従来の Testing Library の書き方とは少し異なりますが、慣れればスムーズに使えます。

React コンポーネントテスト

まず React アダプターをインストールします:

npm install -D @vitest/browser-react

そしてテストを書きます。Counter コンポーネントがあり、ボタンをクリックするとカウントが 1 増えるとします:

// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter'

test('ボタンクリックでカウント', async () => {
  // コンポーネントをブラウザにレンダリング
  await page.mount(<Counter />)

  // ボタン要素を見つける
  const button = page.getByRole('button', { name: 'Count: 0' })

  // ボタンをクリック
  await userEvent.click(button)

  // テキストの変化を検証
  await expect.element(button).toHaveTextContent('Count: 1')
})

Testing Library の書き方と比較してみましょう。Testing Library は render() を使用し、Browser Mode は page.mount() を使用します。Testing Library は screen.getByRole() を使用し、Browser Mode は page.getByRole() を使用します。API は似ており、page オブジェクトだけが Vitest Browser Mode のコンテキストです。

興味深い詳細があります:await expect.element(button)。これは Vitest の Web Testing API で、要素の状態変化を自動的に待機します。手動で await waitFor() する必要はなく、テストフレームワークが非同期待機を処理してくれます。これは Testing Library よりも簡潔です。

Vue コンポーネントテスト

Vue の書き方も同様ですが、Vue アダプターが必要です:

npm install -D @vitest/browser-vue

Vue Counter コンポーネントのテスト:

// Counter.browser.test.ts
import { page } from '@vitest/browser/context'
import { userEvent } from '@vitest/browser/context'
import Counter from './Counter.vue'

test('ボタンクリックでカウント', async () => {
  // Vue コンポーネントをレンダリング
  await page.mount(Counter)

  // ボタンを見つけ、クリックし、検証
  const button = page.getByRole('button', { name: 'Count: 0' })
  await userEvent.click(button)
  await expect.element(button).toHaveTextContent('Count: 1')
})

Vue 2 は @vue/test-utils の書き方を使用し、Browser Mode は直接サポートしていません。ただし、Vue 3 プロジェクトでは、@vitest/browser-vue で十分です。

実際のケース

私のプロジェクトに次のシナリオがありました。HTML5 Drag & Drop API を使用したドラッグ&ドロップソートコンポーネントです。jsdom 環境では、dragstartdrop イベントをシミュレートできません——モックを使用する必要があり、テストできるのは「イベントがトリガーされたかどうか」だけですが、ソートロジックが正しいかどうかはわかりません。

Browser Mode に切り替えると、テストは直接次のように書けます:

test('ドラッグでソート', async () => {
  await page.mount(<SortableList items={['A', 'B', 'C']} />)

  const itemA = page.getByText('A')
  const itemC = page.getByText('C')

  // A を C の後ろにドラッグ
  await userEvent.dragTo(itemA, itemC)

  // 順序の変化を検証
  const items = page.getByRole('listitem')
  await expect.element(items.nth(2)).toHaveTextContent('A')
})

リアルブラウザでは、Drag & Drop API が実際にトリガーされ、ソートロジックが実際に実行され、要素の順序が実際に変化します。テストが通った?なら、本当に問題ありません。

これが Browser Mode の価値です——シミュレーションではなく、実際の動作をテストします。

Playwright vs Browser Mode 選択ガイド

少し混乱するかもしれません。「Browser Mode も Playwright もブラウザテストではないか?違いは何だ?」

簡単に言うと:Browser Mode はコンポーネントをテストし、Playwright はフローをテストします

核心的な違い

特徴Browser ModePlaywright
テスト範囲単一コンポーネントの分離テスト複数ページのフローテスト
実行速度約 200ms/テスト2-5s/テスト
起動コストブラウザインスタンスを共有各テストで独立起動
設定の複雑さ低、Vitest に統合高、独立したプロジェクト設定
適用シナリオ開発段階での迅速な反復リリース前の重要パス検証

Browser Mode の位置づけはコンポーネントテストです——コンポーネントをブラウザにマウントしますが、コンポーネント自体の動作だけをテストします。Playwright の位置づけは E2E です——完全なアプリケーションを開き、ナビゲーション、ログイン、フォーム送信を行い、フロー全体をテストします。

例えれば、Browser Mode は外科医が顕微鏡で単一の細胞を見るようなもので、Playwright は健康診断の医師が臓器システム全体を見るようなものです。それぞれ用途があり、互いに代替できません。

組み合わせ戦略

私の実際のプロジェクトでは、次のように使用しています:

  • Browser Mode:すべての UI コンポーネントをテスト——ボタン、フォーム、カード、モーダル。開発時に書き、コードと一緒にコミット。テストは高速、フィードバックは即座。
  • Playwright E2E:3-5 の重要なパスをテスト——ログイン後のホームページ表示、検索後の結果表示、送信後の注文表示。リリース前、または CI の日次ビルド時のみ実行。

この組み合わせの利点:

  1. Browser Mode は大部分の UI バグをカバーし、開発段階で発見できます。
  2. Playwright はページをまたぐ統合バグをカバーし、リリース前に検証します。
  3. メンテナンスコストが管理可能——コンポーネントテスト 50 個、E2E テスト 5 個、テストを書きすぎて CI を遅くすることはありません。

どちらを選ぶべきか?

簡単な判断基準:

  • Browser Mode を選ぶ:単一コンポーネントの UI 動作をテストしたい——クリック、入力、レンダリング、スタイル。コンポーネントコードが変更されたら、テストは即座にフィードバックが必要。
  • Playwright を選ぶ:複数ページのフローをテストしたい——ログイン → ナビゲーション → 操作 → 検証。または、クロスシステム統合をテストしたい——フロントエンド + バックエンド API + データベース。

例を挙げます。日付選択コンポーネントをテストする場合、日付範囲制限、無効日付、フォーマット表示——Browser Mode を使用します。予約ページで日付を選択し、注文を送信し、支払いページに遷移する場合——Playwright を使用します。

もう一点:Browser Mode はフロントエンドのみをテストしますが、Playwright はフルスタックをテストできます。アプリケーションにバックエンド API がある場合、Playwright E2E はフロントエンド + バックエンドの統合をテストできます。Browser Mode はフロントエンドコンポーネントのみをテストし、バックエンドはモックする必要があります。

CI 環境でのカバレッジゲート

カバレッジ設定は、CI 環境の最後のピースです。正直に言うと、以前は重視していませんでしたが、あるコードリファクタリングでテストカバレッジが 80% から 60% に低下し、本番環境でバグが発生して初めて、カバレッジゲートの重要性に気づきました。

カバレッジ設定

vitest.config.ts にカバレッジ閾値を追加します:

test: {
  coverage: {
    provider: 'v8',  // または 'istanbul'
    reporter: ['text', 'json', 'html'],
    thresholds: {
      lines: 80,
      functions: 80,
      branches: 75,
      statements: 80
    }
  }
}

閾値はどのくらいに設定すべきか?私の経験では:

  • 新規プロジェクト:50% から始め、徐々に上げていく。最初から高すぎると、コードを書くプレッシャーが大きい。
  • 成熟したプロジェクト:80% が妥当な値。コアモジュールはさらに高く、90% または 95% に設定可能。
  • 100% を追求しない:境界ブランチ、例外処理など、テストできないシナリオもあり、無理にテストを書くと時間がかかる。

カバレッジテストを実行:

npx vitest run --coverage

カバレッジが閾値より低い?Vitest はエラーを報告し、CI ビルドが失敗します。これがゲートです——閾値より低いコードは、メインラインにマージできません。

GitHub Actions 統合

CI ワークフローに 2 つのステップを追加します:テスト実行 + カバレッジレポート。

# .github/workflows/test.yml
name: Test

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci
      - run: npx playwright install chromium --with-deps

      - name: Run tests with coverage
        run: npx vitest run --coverage

      - name: Report coverage
        uses: davelosert/vitest-coverage-report-action@v2
        with:
          json-summary-path: './coverage/coverage-summary.json'

この Action は自動的に PR コメントにカバレッジ変化を表示します:

  • カバレッジ変化のパーセンテージ(例:80% から 79%、-1% と表示)
  • どのファイルでカバレッジが低下したか
  • どの新規コードがカバーされていないか

PR 作成者は一目でわかります:「あ、新しく書いたコードのカバレッジが足りない、テストを追加しないと。」

CI 環境での注意点

Browser Mode は CI 環境でいくつかの落とし穴があります:

  1. Playwright ブラウザインストール--with-deps パラメータを追加しないと、Chromium が起動しません。
  2. Headless モード:設定で headless: true を強制。CI 環境にはディスプレイがありません。
  3. タイムアウト設定:Browser Mode テストは jsdom より遅いので、CI のタイムアウト閾値を大きく設定。私は 30 秒に設定しています。
  4. 並列制御:Browser Mode はブラウザインスタンスを共有するため、並列テスト数を高く設定しすぎないこと。私は maxWorkers: 4 に制限しています。

私の CI ワークフロー完全設定:

- name: Run browser tests
  run: npx vitest run --coverage --browser.headless
  env:
    CI: true

--browser.headless はブラウザがウィンドウを開かないことを保証し、CI: true 環境変数で Vitest は一部の動作を自動調整します(例:カラー出力の無効化)。

まとめ

ここまで説明してきましたが、核心は実にシンプルです。jsdom はリアルブラウザの動作をテストできませんが、Browser Mode ならできます。Canvas、CSS 計算スタイル、Web Components、ドラッグ&ドロップ、アニメーション——これらのシナリオでは、Browser Mode が唯一の選択肢です。

設定は難しくありません。@vitest/browser-playwright をインストールし、vitest.config.ts を数行変更すれば、実行できます。React と Vue コンポーネントテストの API は Testing Library とほぼ同じで、学習コストは低いです。

ただし、Browser Mode は万能薬ではありません。単一コンポーネントをテストし、Playwright はフローをテストします。組み合わせて使用します。開発段階では Browser Mode ですべての UI コンポーネントをカバーし、リリース前には Playwright E2E で重要なパスを検証します。これにより、テストカバレッジは包括的で、CI も高速に実行できます。

カバレッジゲートは最後のピースです。閾値を設定し、閾値より低い PR はマージできないようにして、カバレッジの徐々の低下を防ぎます。GitHub Actions に coverage-report-action を追加すれば、PR コメントにカバレッジ変化が自動表示され、一目で問題がわかります。

まだ Browser Mode を試したことがない場合、シンプルなコンポーネントから始めることをお勧めします。例えば、ボタンコンポーネント、入力フィールドコンポーネント。まず Browser Mode を設定し、設定が正しいことを確認してから、徐々に複雑なコンポーネントに拡張していきます。落とし穴にはまることはありますが、落とし穴を乗り越えてテストに慣れれば、開発効率は確実に向上します。

Vitest Browser Mode でコンポーネントテストを設定する

ゼロから Browser Mode を設定し、React/Vue コンポーネントテストを実行し、CI カバレッジゲートを統合します。

⏱️ 目安時間: 20 分

  1. 1

    ステップ1: Playwright プロバイダーをインストール

    npm install -D vitest @vitest/browser-playwright で依存関係をインストールし、npx playwright install chromium でブラウザをダウンロードします。
  2. 2

    ステップ2: vitest.config.ts を設定

    test.browser で provider: playwright()、enabled: true、instances: [{ browser: 'chromium' }] を設定します。CI 環境では headless: true を追加します。
  3. 3

    ステップ3: コンポーネントテストを記述

    page.mount() でコンポーネントをレンダリングし、page.getByRole() で要素をクエリし、userEvent.click() でインタラクションをシミュレートし、expect.element() で結果をアサーションします。
  4. 4

    ステップ4: カバレッジゲートを設定

    vitest.config.ts の coverage.thresholds で閾値(lines: 80)を設定し、GitHub Actions に vitest-coverage-report-action を統合して PR のカバレッジ変化を表示します。

FAQ

Browser Mode と jsdom の違いは何ですか?
jsdom は Node.js 内で DOM をシミュレートしますが、Canvas レンダリング、CSS 計算スタイル、Web Components のライフサイクルはテストできません。Browser Mode はリアルブラウザでコンポーネントをレンダリングし、実際の動作をテストします。
Browser Mode と Playwright E2E はどう使い分けるべきですか?
Browser Mode は単一コンポーネントのテストに適しており(200ms/テスト)、開発段階で即座にフィードバックを得られます。Playwright は複数ページのフローテストに適しており(2-5s/テスト)、リリース前に重要なパスを検証します。組み合わせて使用することをお勧めします。
カバレッジ閾値はどのくらいに設定すべきですか?
新規プロジェクトは 50% から始め、成熟したプロジェクトは 80% が妥当です。コアモジュールは 90% に設定できます。100% を目指す必要はありません。テストできないエッジケースも存在するからです。
Browser Mode は Vue 2 に対応していますか?
対応していません。Vue 2 では @vue/test-utils を使用する必要があります。Vue 3 プロジェクトでは @vitest/browser-vue を直接使用できます。
CI 環境で Browser Mode を使用する際の注意点は?
Playwright インストール時に --with-deps パラメータを追加し、headless: true を設定し、タイムアウト閾値を大きくし(30秒)、並列テスト数を制限します(maxWorkers: 4)。

6 min read · 公開日: 2026年5月17日 · 更新日: 2026年5月17日

コメント

GitHubアカウントでログインしてコメントできます