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:カスタムエレメントの
connectedCallback、disconnectedCallbackは 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 環境では、dragstart、drop イベントをシミュレートできません——モックを使用する必要があり、テストできるのは「イベントがトリガーされたかどうか」だけですが、ソートロジックが正しいかどうかはわかりません。
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 Mode | Playwright |
|---|---|---|
| テスト範囲 | 単一コンポーネントの分離テスト | 複数ページのフローテスト |
| 実行速度 | 約 200ms/テスト | 2-5s/テスト |
| 起動コスト | ブラウザインスタンスを共有 | 各テストで独立起動 |
| 設定の複雑さ | 低、Vitest に統合 | 高、独立したプロジェクト設定 |
| 適用シナリオ | 開発段階での迅速な反復 | リリース前の重要パス検証 |
Browser Mode の位置づけはコンポーネントテストです——コンポーネントをブラウザにマウントしますが、コンポーネント自体の動作だけをテストします。Playwright の位置づけは E2E です——完全なアプリケーションを開き、ナビゲーション、ログイン、フォーム送信を行い、フロー全体をテストします。
例えれば、Browser Mode は外科医が顕微鏡で単一の細胞を見るようなもので、Playwright は健康診断の医師が臓器システム全体を見るようなものです。それぞれ用途があり、互いに代替できません。
組み合わせ戦略
私の実際のプロジェクトでは、次のように使用しています:
- Browser Mode:すべての UI コンポーネントをテスト——ボタン、フォーム、カード、モーダル。開発時に書き、コードと一緒にコミット。テストは高速、フィードバックは即座。
- Playwright E2E:3-5 の重要なパスをテスト——ログイン後のホームページ表示、検索後の結果表示、送信後の注文表示。リリース前、または CI の日次ビルド時のみ実行。
この組み合わせの利点:
- Browser Mode は大部分の UI バグをカバーし、開発段階で発見できます。
- Playwright はページをまたぐ統合バグをカバーし、リリース前に検証します。
- メンテナンスコストが管理可能——コンポーネントテスト 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 環境でいくつかの落とし穴があります:
- Playwright ブラウザインストール:
--with-depsパラメータを追加しないと、Chromium が起動しません。 - Headless モード:設定で
headless: trueを強制。CI 環境にはディスプレイがありません。 - タイムアウト設定:Browser Mode テストは jsdom より遅いので、CI のタイムアウト閾値を大きく設定。私は 30 秒に設定しています。
- 並列制御: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: Playwright プロバイダーをインストール
npm install -D vitest @vitest/browser-playwright で依存関係をインストールし、npx playwright install chromium でブラウザをダウンロードします。 - 2
ステップ2: vitest.config.ts を設定
test.browser で provider: playwright()、enabled: true、instances: [{ browser: 'chromium' }] を設定します。CI 環境では headless: true を追加します。 - 3
ステップ3: コンポーネントテストを記述
page.mount() でコンポーネントをレンダリングし、page.getByRole() で要素をクエリし、userEvent.click() でインタラクションをシミュレートし、expect.element() で結果をアサーションします。 - 4
ステップ4: カバレッジゲートを設定
vitest.config.ts の coverage.thresholds で閾値(lines: 80)を設定し、GitHub Actions に vitest-coverage-report-action を統合して PR のカバレッジ変化を表示します。
FAQ
Browser Mode と jsdom の違いは何ですか?
Browser Mode と Playwright E2E はどう使い分けるべきですか?
カバレッジ閾値はどのくらいに設定すべきですか?
Browser Mode は Vue 2 に対応していますか?
CI 環境で Browser Mode を使用する際の注意点は?
6 min read · 公開日: 2026年5月17日 · 更新日: 2026年5月17日
Vitest テストガイド
このページはシリーズの最初の記事です。次の記事へ進むか、シリーズ全体ページで全体像を確認できます。
前の記事
シリーズの最初の記事です。
次の記事
現時点ではこれがシリーズの最新記事です。
関連記事
GitHub Actions セキュリティ実践:tj-actions インシデントから学ぶ3つの重要な防御策
GitHub Actions セキュリティ実践:tj-actions インシデントから学ぶ3つの重要な防御策
Nginx パフォーマンスチューニング実践:gzip、キャッシュ、接続プール設定
Nginx パフォーマンスチューニング実践:gzip、キャッシュ、接続プール設定
Docker ネットワークモード選択実践:bridge、host、overlay の決定ガイド
コメント
GitHubアカウントでログインしてコメントできます