テスト環境比較:Vitest vs Jest vs Playwright CT ― Vite プロジェクトの最適解
Vite プロジェクトでテスト環境を構築する際、Vitest、Jest、Playwright Component Testing(以下 Playwright CT)の 3 つが主要な選択肢として挙げられます。それぞれに特徴があり、プロジェクトの要件や開発体制によって最適な選択は異なります。
本記事では、これら 3 つのテストツールを徹底的に比較し、Vite プロジェクトに最適なテスト環境の選び方を解説していきます。セットアップの容易さ、実行速度、機能の充実度、そして実際の開発現場での使い勝手まで、具体的なコード例を交えながら詳しくご紹介します。
背景
Vite とテスト環境の関係性
Vite は ESM(ECMAScript Modules)ネイティブなビルドツールとして、従来の Webpack ベースのツールとは異なるアーキテクチャを採用しています。開発時は esbuild による高速なトランスパイルを行い、本番ビルドでは Rollup を利用する仕組みです。
この特性により、従来の Node.js 環境で動作するテストツールとの間に互換性の課題が生じることがあります。そのため、Vite プロジェクト専用に最適化されたテストツールが求められるようになりました。
以下の図は、Vite プロジェクトとテストツールの関係性を示しています。
mermaidflowchart TB
  viteProject["Vite プロジェクト<br/>(ESM ネイティブ)"]
  devServer["開発サーバー<br/>(esbuild)"]
  prodBuild["本番ビルド<br/>(Rollup)"]
  vitest["Vitest<br/>(Vite ネイティブ)"]
  jest["Jest<br/>(CommonJS ベース)"]
  playwright["Playwright CT<br/>(ブラウザ実行)"]
  viteProject --> devServer
  viteProject --> prodBuild
  viteProject -.->|直接統合| vitest
  viteProject -.->|トランスパイル必要| jest
  viteProject -.->|コンポーネント実行| playwright
  vitest -->|設定共有| viteProject
  jest -->|別途設定| viteProject
  playwright -->|Vite 連携| viteProject
図で理解できる要点:
- Vite は開発と本番で異なるツール(esbuild/Rollup)を使い分ける
 - Vitest は Vite の設定をそのまま利用できる直接統合型
 - Jest は CommonJS ベースのため別途トランスパイル設定が必要
 - Playwright CT はブラウザ上でコンポーネントを実際に動かす
 
各テストツールの誕生背景
Vitest の誕生
Vitest は Vite のエコシステムとして 2021 年に登場しました。開発者は Vue.js の作者である Evan You 氏のチームです。
Vite の設定をそのまま流用でき、ESM を完全サポートする点が最大の特徴となっています。Jest 互換の API を提供しているため、既存の Jest テストコードの移行も比較的容易です。
Jest の歴史
Jest は Facebook(現 Meta)が開発した、JavaScript エコシスタムで最も広く使われているテストフレームワークの 1 つです。2014 年に登場し、React プロジェクトの標準テストツールとして普及しました。
設定なしで動作する「ゼロコンフィグ」を掲げ、スナップショットテストやカバレッジレポートなど、豊富な機能を標準搭載しています。
Playwright CT の特徴
Playwright Component Testing は Microsoft が開発した Playwright の一部として 2022 年に追加されました。従来の E2E テストツールとしての Playwright に、コンポーネント単位のテスト機能が加わったものです。
実際のブラウザ環境でコンポーネントをマウントし、ユーザーの操作を再現できる点が強みとなっています。
課題
Vite プロジェクトでのテスト環境構築における 3 つの課題
Vite プロジェクトでテスト環境を選定する際、以下のような課題に直面します。
課題 1:ESM との互換性
Vite は ESM ネイティブな設計のため、CommonJS を前提としたテストツールでは追加の設定やトランスパイルが必要になります。特に、動的インポートや import.meta といった ESM 固有の機能を使用している場合、互換性の問題が顕著です。
javascript// Vite プロジェクトでよく見る ESM の記法
const modules = import.meta.glob('./components/*.jsx');
const envVar = import.meta.env.VITE_API_URL;
このようなコードは Jest では標準では動作せず、追加の設定が必要になります。
課題 2:テスト実行速度
開発体験(DX)において、テストの実行速度は重要な要素です。変更を加えるたびに数十秒も待たされるようでは、開発のリズムが崩れてしまいます。
Vite が高速な開発サーバーを実現しているのと同様に、テスト環境にも高速性が求められます。しかし、テストツールによって実行速度には大きな差があるのが現実です。
課題 3:設定の複雑さと保守性
理想的には、Vite の設定とテスト環境の設定を統一し、設定ファイルの重複を避けたいところです。しかし、テストツールによっては Vite とは別に独自の設定が必要になり、保守の負担が増大します。
以下の図は、各テストツールの設定管理の違いを示しています。
mermaidflowchart LR
  viteConfig["vite.config.ts<br/>(Vite 設定)"]
  subgraph vitestFlow["Vitest の場合"]
    vitestConfig["vite.config.ts<br/>(テスト設定も統合)"]
  end
  subgraph jestFlow["Jest の場合"]
    jestConfig["jest.config.js"]
    transform["トランスパイル設定"]
    jestConfig --> transform
  end
  subgraph playwrightFlow["Playwright CT の場合"]
    pwConfig["playwright-ct.config.ts"]
    pwVite["Vite 連携設定"]
    pwConfig --> pwVite
  end
  viteConfig -->|設定共有| vitestConfig
  viteConfig -.->|別管理| jestConfig
  viteConfig -.->|一部共有| pwConfig
  style vitestConfig fill:#d4f1d4
  style jestConfig fill:#ffd4d4
  style pwConfig fill:#d4e5ff
図で理解できる要点:
- Vitest は Vite の設定ファイルを直接利用できる(最もシンプル)
 - Jest は独自の設定ファイルが必要で、トランスパイル設定も追加
 - Playwright CT は専用設定ファイルを持つが、Vite との連携機能あり
 
以下の表で、各テストツールが抱える課題を整理します。
| # | 課題項目 | Vitest | Jest | Playwright CT | 
|---|---|---|---|---|
| 1 | ESM ネイティブ対応 | ◎ 完全対応 | △ 追加設定必要 | ◎ ブラウザで実行 | 
| 2 | 実行速度 | ◎ 非常に高速 | ○ 中程度 | △ やや遅い | 
| 3 | Vite 設定の共有 | ◎ 完全共有 | × 別途必要 | ○ 一部共有可能 | 
| 4 | セットアップの容易さ | ◎ 非常に簡単 | △ やや複雑 | △ やや複雑 | 
| 5 | 学習コスト | ○ Jest 互換 API | ◎ 資料豊富 | △ 独自 API | 
この表から分かるように、それぞれのツールには一長一短があります。次のセクションでは、これらの課題に対する具体的な解決策と、各ツールの詳細な比較を行っていきます。
解決策
それぞれのテストツールが解決する課題と適用シーン
3 つのテストツールは、それぞれ異なるアプローチで Vite プロジェクトのテスト課題を解決します。ここでは各ツールの特徴と、どのような場合に選択すべきかを詳しく解説していきます。
Vitest:Vite ネイティブな高速テスト環境
Vitest の解決アプローチ
Vitest は「Vite のための Vite によるテストツール」として設計されています。Vite の設定をそのまま使用できるため、エイリアス設定やプラグイン、環境変数などをテストコードでも同じように扱えます。
また、esbuild ベースの高速なトランスパイルと、Vite の HMR(ホットモジュールリプレースメント)技術を応用したウォッチモードにより、テスト実行速度が飛躍的に向上しています。
Vitest の主な機能
mermaidflowchart TB
  vitest["Vitest"]
  features1["Jest 互換 API<br/>(describe, test, expect)"]
  features2["Vite 設定共有<br/>(プラグイン・エイリアス)"]
  features3["UI モード<br/>(ブラウザ UI)"]
  features4["カバレッジ<br/>(c8/istanbul)"]
  speed1["esbuild<br/>トランスパイル"]
  speed2["スマート<br/>ウォッチモード"]
  speed3["並列実行<br/>(マルチスレッド)"]
  vitest --> features1
  vitest --> features2
  vitest --> features3
  vitest --> features4
  vitest --> speed1
  vitest --> speed2
  vitest --> speed3
  style vitest fill:#a8e6cf
図で理解できる要点:
- Jest 互換 API により既存コードの移行が容易
 - Vite 設定をそのまま利用できるため設定の重複なし
 - UI モードで視覚的なテスト管理が可能
 - esbuild による高速トランスパイルと並列実行で高速化を実現
 
Vitest が適しているケース
以下のような場合、Vitest は最も適した選択肢となります。
- 新規 Vite プロジェクト:設定の統一により開発効率が最大化されます
 - テスト実行速度を重視:大量のテストケースを高速に実行したい場合
 - モダンな開発環境:ESM、TypeScript、最新の JS 機能を積極的に使いたい場合
 - 設定の簡素化:テストツール用の複雑な設定を避けたい場合
 
Jest:実績豊富な標準的テストフレームワーク
Jest の解決アプローチ
Jest は長年の実績により、豊富なエコシステムと充実したドキュメントを持っています。Vite との直接的な統合はありませんが、トランスフォーム設定により Vite プロジェクトでも利用可能です。
スナップショットテスト、モック機能、カバレッジレポートなど、テストに必要な機能がすべて揃っており、「ゼロコンフィグ」の理念のもと、最小限の設定で動作します。
Jest の主な機能
mermaidflowchart TB
  jest["Jest"]
  core1["スナップショット<br/>テスト"]
  core2["強力なモック<br/>機能"]
  core3["並列実行<br/>(ワーカー)"]
  core4["カバレッジ<br/>レポート"]
  eco1["豊富な<br/>プラグイン"]
  eco2["充実した<br/>ドキュメント"]
  eco3["大規模<br/>コミュニティ"]
  jest --> core1
  jest --> core2
  jest --> core3
  jest --> core4
  jest --> eco1
  jest --> eco2
  jest --> eco3
  style jest fill:#ffdfba
図で理解できる要点:
- スナップショットテストで UI の変更を自動検出
 - モック機能により外部依存を簡単にシミュレート
 - 大規模コミュニティによる豊富な情報とプラグイン
 - カバレッジレポートで網羅率を可視化
 
Jest が適しているケース
以下のような場合、Jest が適切な選択となるでしょう。
- 既存の Jest テストコードがある:移行コストを最小限にしたい場合
 - チームに Jest の知見がある:学習コストを抑えられます
 - スナップショットテストを多用:UI コンポーネントの回帰テストに有効
 - エコシステムの充実を重視:サードパーティプラグインを活用したい場合
 
Playwright CT:実ブラウザでのコンポーネントテスト
Playwright CT の解決アプローチ
Playwright Component Testing は、実際のブラウザ環境でコンポーネントをテストする点が最大の特徴です。jsdom などのシミュレーション環境ではなく、Chrome、Firefox、Safari などの実ブラウザでコンポーネントを動作させます。
これにより、CSS のレンダリング、ブラウザ固有の動作、アクセシビリティなど、より実環境に近い状態でのテストが可能になります。
Playwright CT の主な機能
mermaidflowchart LR
  component["コンポーネント<br/>(React/Vue/Svelte)"]
  mount["Playwright CT<br/>マウント"]
  browsers["実ブラウザ実行"]
  chrome["Chromium"]
  firefox["Firefox"]
  webkit["WebKit<br/>(Safari)"]
  test["テスト実行"]
  interact["ユーザー操作<br/>シミュレート"]
  snapshot["ビジュアル<br/>回帰テスト"]
  a11y["アクセシビリティ<br/>検証"]
  component --> mount
  mount --> browsers
  browsers --> chrome
  browsers --> firefox
  browsers --> webkit
  chrome --> test
  firefox --> test
  webkit --> test
  test --> interact
  test --> snapshot
  test --> a11y
  style browsers fill:#dae8fc
  style test fill:#d5e8d4
図で理解できる要点:
- コンポーネントを実際のブラウザ上にマウント
 - 複数ブラウザ(Chromium、Firefox、WebKit)でクロスブラウザテスト
 - ユーザー操作を忠実に再現できる
 - ビジュアル回帰テストやアクセシビリティ検証も可能
 
Playwright CT が適しているケース
以下のような場合、Playwright CT が最適な選択肢となります。
- ブラウザ固有の動作を検証:CSS レンダリングやブラウザ API を実際に確認したい場合
 - ビジュアル回帰テスト:UI の視覚的な変更を自動検出したい場合
 - クロスブラウザテスト:複数ブラウザでの動作保証が必要な場合
 - アクセシビリティ検証:実ブラウザでの支援技術との互換性を確認したい場合
 - E2E テストとの統一:既に Playwright で E2E テストを行っている場合
 
テストの種類による使い分け
実際のプロジェクトでは、テストの種類によって使い分けることも有効です。
| # | テストの種類 | 推奨ツール | 理由 | 
|---|---|---|---|
| 1 | ユニットテスト(関数・ロジック) | Vitest | 高速実行が可能で、大量のテストに適する | 
| 2 | コンポーネントテスト(基本動作) | Vitest + Testing Library | jsdom で十分な場合は高速な Vitest | 
| 3 | コンポーネントテスト(視覚的) | Playwright CT | 実ブラウザでの正確な検証が必要な場合 | 
| 4 | インテグレーションテスト | Vitest または Jest | モック機能の充実度で選択 | 
| 5 | E2E テスト | Playwright | フルページのユーザーフロー検証 | 
次のセクションでは、これら 3 つのテストツールの具体的なセットアップ方法と、実際のテストコード例を見ていきます。
具体例
各テストツールのセットアップと実践的なテストコード
ここでは、実際に Vite プロジェクトへ各テストツールを導入し、同じコンポーネントをテストする例を通して、それぞれの違いを具体的に見ていきます。
前提:テスト対象のコンポーネント
まず、テスト対象となるシンプルなカウンターコンポーネントを用意します。このコンポーネントを 3 つのテストツールでそれぞれテストしていきます。
コンポーネントの実装
typescript// src/components/Counter.tsx
import { useState } from 'react';
interface CounterProps {
  initialCount?: number;
  step?: number;
}
export const Counter: React.FC<CounterProps> = ({
  initialCount = 0,
  step = 1,
}) => {
  const [count, setCount] = useState(initialCount);
  return (
    <div className='counter'>
      <h2>カウンター</h2>
      <p data-testid='count-value'>
        現在のカウント: {count}
      </p>
      <button
        data-testid='increment-btn'
        onClick={() => setCount(count + step)}
      >
        +{step} 増やす
      </button>
      <button
        data-testid='decrement-btn'
        onClick={() => setCount(count - step)}
      >
        -{step} 減らす
      </button>
      <button
        data-testid='reset-btn'
        onClick={() => setCount(initialCount)}
      >
        リセット
      </button>
    </div>
  );
};
このコンポーネントは、カウント値の表示、増減ボタン、リセット機能を持つシンプルなものです。data-testid 属性を付与することで、テストでの要素特定を容易にしています。
パターン 1:Vitest でのセットアップとテスト
Vitest のインストール
bash# Vitest 本体と関連パッケージをインストール
yarn add -D vitest @vitest/ui
# React Testing Library を追加(コンポーネントテスト用)
yarn add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
# jsdom を追加(ブラウザ環境のシミュレート用)
yarn add -D jsdom
各パッケージの役割は以下の通りです。
vitest:テストランナー本体@vitest/ui:ブラウザベースの UI インターフェース@testing-library/react:React コンポーネントのテストユーティリティ@testing-library/jest-dom:DOM アサーション拡張@testing-library/user-event:ユーザー操作のシミュレートjsdom:Node.js 環境での DOM 実装
Vite 設定ファイルの更新
Vitest は Vite の設定ファイルをそのまま利用できます。vite.config.ts に test セクションを追加するだけで設定完了です。
typescript// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
  plugins: [react()],
  // Vitest の設定を追加
  test: {
    globals: true, // describe, test, expect をグローバルに使用可能にする
    environment: 'jsdom', // ブラウザ環境をシミュレート
    setupFiles: './src/test/setup.ts', // テスト前の初期設定ファイル
    css: true, // CSS のインポートを有効化
  },
});
この設定により、Vite のプラグイン設定やエイリアス設定がテストでもそのまま使用できます。
セットアップファイルの作成
テスト実行前に必要な初期設定を行うファイルを作成します。
typescript// src/test/setup.ts
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
// jest-dom のマッチャーを Vitest に追加
expect.extend(matchers);
// 各テスト後にクリーンアップを実行
afterEach(() => {
  cleanup();
});
このファイルで toBeInTheDocument() などの便利なマッチャーが使えるようになります。
Vitest でのテストコード
それでは、実際のテストコードを見ていきましょう。
typescript// src/components/Counter.test.tsx
import { describe, test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
まず、必要な関数やライブラリをインポートします。vitest からテスト用の関数、@testing-library/react からレンダリングとクエリ用の関数を読み込んでいます。
typescriptdescribe('Counter コンポーネント', () => {
  test('初期値が正しく表示される', () => {
    render(<Counter initialCount={10} />);
    const countValue = screen.getByTestId('count-value');
    expect(countValue).toHaveTextContent(
      '現在のカウント: 10'
    );
  });
});
このテストでは、コンポーネントをレンダリングし、初期値が正しく表示されることを確認しています。
typescripttest('増加ボタンをクリックするとカウントが増える', async () => {
  const user = userEvent.setup();
  render(<Counter step={2} />);
  const incrementBtn = screen.getByTestId('increment-btn');
  const countValue = screen.getByTestId('count-value');
  // 初期値は 0
  expect(countValue).toHaveTextContent('現在のカウント: 0');
  // クリック後は 2 増える
  await user.click(incrementBtn);
  expect(countValue).toHaveTextContent('現在のカウント: 2');
  // さらにクリックすると 4 になる
  await user.click(incrementBtn);
  expect(countValue).toHaveTextContent('現在のカウント: 4');
});
ユーザーのクリック操作をシミュレートし、カウントが正しく増加することを検証しています。userEvent は非同期で動作するため、await を使用します。
typescript  test('減少ボタンをクリックするとカウントが減る', async () => {
    const user = userEvent.setup()
    render(<Counter initialCount={5} step={1} />)
    const decrementBtn = screen.getByTestId('decrement-btn')
    const countValue = screen.getByTestId('count-value')
    await user.click(decrementBtn)
    expect(countValue).toHaveTextContent('現在のカウント: 4')
  })
  test('リセットボタンで初期値に戻る', async () => {
    const user = userEvent.setup()
    render(<Counter initialCount={0} />)
    const incrementBtn = screen.getByTestId('increment-btn')
    const resetBtn = screen.getByTestId('reset-btn')
    const countValue = screen.getByTestId('count-value')
    // カウントを増やす
    await user.click(incrementBtn)
    await user.click(incrementBtn)
    expect(countValue).toHaveTextContent('現在のカウント: 2')
    // リセットで 0 に戻る
    await user.click(resetBtn)
    expect(countValue).toHaveTextContent('現在のカウント: 0')
  })
})
減少機能とリセット機能のテストも同様の手法で記述できます。
package.json へのスクリプト追加
json{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:run": "vitest run",
    "test:coverage": "vitest --coverage"
  }
}
これらのコマンドで、ウォッチモード、UI モード、1 回実行、カバレッジ計測などが可能になります。
Vitest の実行結果
bashyarn test
# 出力例
 ✓ src/components/Counter.test.tsx (4)
   ✓ Counter コンポーネント (4)
     ✓ 初期値が正しく表示される
     ✓ 増加ボタンをクリックするとカウントが増える
     ✓ 減少ボタンをクリックするとカウントが減る
     ✓ リセットボタンで初期値に戻る
 Test Files  1 passed (1)
      Tests  4 passed (4)
   Start at  10:30:15
   Duration  342ms
Vitest は非常に高速で、この例では 342ms で全テストが完了しています。
パターン 2:Jest でのセットアップとテスト
Jest のインストール
bash# Jest 本体と React 用の設定をインストール
yarn add -D jest @types/jest ts-jest
# Babel を使った変換設定
yarn add -D @babel/preset-env @babel/preset-react @babel/preset-typescript
# React Testing Library
yarn add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
# jsdom 環境
yarn add -D jest-environment-jsdom
Jest では Vite の変換機構を使えないため、Babel または ts-jest によるトランスパイルが必要になります。
Jest 設定ファイルの作成
Jest は独自の設定ファイルが必要です。Vite の設定とは別に管理する必要があります。
javascript// jest.config.js
module.exports = {
  // テスト環境として jsdom を使用
  testEnvironment: 'jsdom',
  // TypeScript と JSX のトランスパイル設定
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },
  // モジュール名の解決設定(Vite のエイリアスと合わせる)
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
};
この設定では、TypeScript ファイルを ts-jest で変換し、CSS インポートをモック化しています。
javascript  // セットアップファイルの指定
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
  // テストファイルのパターン
  testMatch: [
    '**/__tests__/**/*.{ts,tsx}',
    '**/*.{spec,test}.{ts,tsx}',
  ],
  // カバレッジ設定
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/main.tsx',
  ],
テスト対象のファイルパターンや、カバレッジ計測の対象を指定します。
Babel 設定ファイルの作成
javascript// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    ['@babel/preset-react', { runtime: 'automatic' }],
    '@babel/preset-typescript',
  ],
};
React の JSX と TypeScript を変換するための Babel 設定です。Vite では不要でしたが、Jest では必須となります。
Jest でのテストコード
テストコード自体は Vitest とほぼ同じです。Jest 互換 API のおかげで、移行が容易になっています。
typescript// src/components/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter コンポーネント', () => {
  test('初期値が正しく表示される', () => {
    render(<Counter initialCount={10} />);
    const countValue = screen.getByTestId('count-value');
    expect(countValue).toHaveTextContent(
      '現在のカウント: 10'
    );
  });
  test('増加ボタンをクリックするとカウントが増える', async () => {
    const user = userEvent.setup();
    render(<Counter step={2} />);
    const incrementBtn =
      screen.getByTestId('increment-btn');
    await user.click(incrementBtn);
    const countValue = screen.getByTestId('count-value');
    expect(countValue).toHaveTextContent(
      '現在のカウント: 2'
    );
  });
});
コードの内容は Vitest の例とほぼ同一です。主な違いは、インポート元が vitest ではなく自動的にグローバルに提供される点です。
package.json へのスクリプト追加
json{
  "scripts": {
    "test": "jest --watch",
    "test:run": "jest",
    "test:coverage": "jest --coverage"
  }
}
Jest の実行結果
bashyarn test:run
# 出力例
 PASS  src/components/Counter.test.tsx
  Counter コンポーネント
    ✓ 初期値が正しく表示される (45ms)
    ✓ 増加ボタンをクリックするとカウントが増える (78ms)
    ✓ 減少ボタンをクリックするとカウントが減る (65ms)
    ✓ リセットボタンで初期値に戻る (82ms)
Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.145s
Vitest と比較すると、実行時間が約 6 倍かかっています(342ms → 2145ms)。これは Babel によるトランスパイルのオーバーヘッドが主な原因です。
パターン 3:Playwright CT でのセットアップとテスト
Playwright CT のインストール
bash# Playwright と Component Testing パッケージをインストール
yarn add -D @playwright/experimental-ct-react
# 初期設定の自動生成
yarn playwright install
Playwright の初回インストール時には、各ブラウザのバイナリが自動的にダウンロードされます。
Playwright CT 設定ファイルの作成
Playwright CT は専用の設定ファイルを持ちますが、Vite との連携機能があります。
typescript// playwright-ct.config.ts
import { defineConfig, devices } from '@playwright/experimental-ct-react'
export default defineConfig({
  // テストファイルのパターン
  testDir: './src',
  testMatch: '**/*.ct.{ts,tsx}',
  // スナップショットの保存先
  snapshotDir: './src/test/__snapshots__',
設定ファイルの前半では、テストファイルの場所とスナップショットの保存場所を指定します。
typescript  // Vite の設定を使用
  use: {
    ctViteConfig: {
      // vite.config.ts の内容を参照可能
    },
  },
  // 複数ブラウザでテスト
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
})
ctViteConfig で Vite の設定を利用でき、projects で複数ブラウザでのテストが可能です。
Playwright CT でのテストコード
Playwright CT のテストは、実ブラウザでコンポーネントをマウントする点が特徴的です。
typescript// src/components/Counter.ct.tsx
import {
  test,
  expect,
} from '@playwright/experimental-ct-react';
import { Counter } from './Counter';
インポートは Playwright 専用のものを使用します。
typescripttest('初期値が正しく表示される', async ({ mount }) => {
  const component = await mount(
    <Counter initialCount={10} />
  );
  await expect(
    component.getByTestId('count-value')
  ).toContainText('現在のカウント: 10');
});
mount 関数で実際のブラウザにコンポーネントをマウントします。すべての操作が非同期のため、await が必要です。
typescripttest('増加ボタンをクリックするとカウントが増える', async ({
  mount,
}) => {
  const component = await mount(<Counter step={2} />);
  const incrementBtn =
    component.getByTestId('increment-btn');
  const countValue = component.getByTestId('count-value');
  // 初期状態を確認
  await expect(countValue).toContainText(
    '現在のカウント: 0'
  );
  // クリック操作
  await incrementBtn.click();
  await expect(countValue).toContainText(
    '現在のカウント: 2'
  );
  // さらにクリック
  await incrementBtn.click();
  await expect(countValue).toContainText(
    '現在のカウント: 4'
  );
});
ボタンのクリックも実際のブラウザイベントとして発火されるため、より実環境に近いテストになります。
typescripttest('リセットボタンで初期値に戻る', async ({ mount }) => {
  const component = await mount(
    <Counter initialCount={0} />
  );
  // カウントを増やす
  await component.getByTestId('increment-btn').click();
  await component.getByTestId('increment-btn').click();
  // リセット
  await component.getByTestId('reset-btn').click();
  await expect(
    component.getByTestId('count-value')
  ).toContainText('現在のカウント: 0');
});
ビジュアル回帰テストの追加
Playwright CT の強みであるビジュアル回帰テストも簡単に追加できます。
typescripttest('コンポーネントの見た目が変わっていない', async ({
  mount,
}) => {
  const component = await mount(<Counter />);
  // スクリーンショットを撮影して前回と比較
  await expect(component).toHaveScreenshot(
    'counter-initial.png'
  );
  // クリック後の状態もスクリーンショット
  await component.getByTestId('increment-btn').click();
  await expect(component).toHaveScreenshot(
    'counter-after-click.png'
  );
});
このテストでは、コンポーネントのスクリーンショットを撮影し、前回のスクリーンショットと比較します。視覚的な変更があった場合、テストが失敗します。
package.json へのスクリプト追加
json{
  "scripts": {
    "test:ct": "playwright test -c playwright-ct.config.ts",
    "test:ct:ui": "playwright test -c playwright-ct.config.ts --ui",
    "test:ct:debug": "playwright test -c playwright-ct.config.ts --debug"
  }
}
Playwright CT の実行結果
bashyarn test:ct
# 出力例
Running 12 tests using 3 workers
  ✓  [chromium] › Counter.ct.tsx:3:1 › 初期値が正しく表示される (234ms)
  ✓  [chromium] › Counter.ct.tsx:9:1 › 増加ボタンをクリックするとカウントが増える (456ms)
  ✓  [chromium] › Counter.ct.tsx:23:1 › リセットボタンで初期値に戻る (389ms)
  ✓  [firefox] › Counter.ct.tsx:3:1 › 初期値が正しく表示される (267ms)
  ✓  [firefox] › Counter.ct.tsx:9:1 › 増加ボタンをクリックするとカウントが増える (478ms)
  ✓  [firefox] › Counter.ct.tsx:23:1 › リセットボタンで初期値に戻る (412ms)
  ✓  [webkit] › Counter.ct.tsx:3:1 › 初期値が正しく表示される (298ms)
  ✓  [webkit] › Counter.ct.tsx:9:1 › 増加ボタンをクリックするとカウントが増える (523ms)
  ✓  [webkit] › Counter.ct.tsx:23:1 › リセットボタンで初期値に戻る (445ms)
  12 passed (4.2s)
3 つのブラウザでそれぞれテストが実行され、クロスブラウザでの動作が保証されます。実ブラウザの起動が必要なため、実行時間は最も長くなります。
3 つのテストツールの比較まとめ
実際のコード例を見てきた上で、改めて比較表を確認しましょう。
| # | 比較項目 | Vitest | Jest | Playwright CT | 
|---|---|---|---|---|
| 1 | セットアップの容易さ | ◎ Vite 設定に追記のみ | △ 複数の設定ファイル必要 | ○ 専用設定ファイル 1 つ | 
| 2 | 実行速度(4 テスト) | ◎ 342ms | ○ 2145ms | △ 4200ms | 
| 3 | テストコードの書きやすさ | ◎ Jest 互換で直感的 | ◎ 充実した API | ○ 非同期処理が多い | 
| 4 | 実環境との近さ | △ jsdom シミュレート | △ jsdom シミュレート | ◎ 実ブラウザ | 
| 5 | ビジュアルテスト | × 非対応 | ○ スナップショット | ◎ スクリーンショット比較 | 
| 6 | クロスブラウザテスト | × 非対応 | × 非対応 | ◎ 3 ブラウザ対応 | 
| 7 | 学習コスト | ○ Jest と同じ API | ◎ 情報豊富 | △ 独自 API 多い | 
| 8 | エコシステム | ○ 成長中 | ◎ 非常に充実 | ○ Playwright と統合 | 
この表から、用途に応じた最適な選択が見えてきます。高速なユニットテストには Vitest、実ブラウザでの検証には Playwright CT、既存資産の活用には Jest が適しているでしょう。
まとめ
Vite プロジェクトにおけるテスト環境の選択は、プロジェクトの性質や開発チームの状況によって最適解が異なります。本記事では、Vitest、Jest、Playwright CT の 3 つを詳しく比較してきました。
各ツールの推奨シーン
Vitest を選ぶべきケース:
新規の Vite プロジェクトで、高速なテスト実行と設定の簡素化を重視する場合に最適です。ESM ネイティブな環境で開発しており、Vite の設定をそのまま活用したい場合、Vitest が第一選択肢となるでしょう。
特に、大量のユニットテストを頻繁に実行する開発スタイルでは、その高速性が開発体験を大きく向上させます。
Jest を選ぶべきケース:
既存の Jest テストコードがあり、それを活かしながら Vite へ移行したい場合や、チームに Jest の知見が蓄積されている場合に適しています。また、豊富なエコシステムやプラグインを活用したい場合も Jest が有力な選択肢です。
スナップショットテストを中心としたテスト戦略を採用している場合、Jest の成熟した機能が役立ちます。
Playwright CT を選ぶべきケース:
実際のブラウザ環境での動作保証が必要な場合、特に CSS のレンダリングやブラウザ固有の動作を検証したい場合に最適です。ビジュアル回帰テストやクロスブラウザテストが必要なプロジェクトでは、Playwright CT の強みが最大限に発揮されます。
また、既に Playwright で E2E テストを行っている場合、コンポーネントテストも統一できる利点があります。
組み合わせて使う戦略
実際のプロジェクトでは、複数のテストツールを組み合わせることも有効な戦略となります。
例えば、以下のような組み合わせが考えられます。
- ユニットテストとロジックテストは Vitest で高速に実行
 - コンポーネントの基本動作テストも Vitest + Testing Library で実施
 - 重要な UI コンポーネントのビジュアル回帰テストは Playwright CT で厳密に検証
 - E2E テストは Playwright で実施
 
この組み合わせにより、各ツールの強みを活かしながら、効率的で信頼性の高いテスト環境を構築できます。
テスト環境選定のチェックリスト
最後に、テスト環境を選定する際のチェックリストをご紹介します。
- プロジェクトの規模:小〜中規模なら Vitest、大規模で既存資産があるなら Jest も検討
 - テスト実行頻度:頻繁に実行するなら高速な Vitest が有利
 - 実ブラウザでの検証の必要性:CSS や視覚的な検証が重要なら Playwright CT
 - チームの知見:既存の知識を活かせるツールを選ぶことも重要
 - CI/CD 環境:実行時間がビルドパイプラインに与える影響を考慮
 - 保守性:設定ファイルの管理負担を考慮
 
Vite プロジェクトでのテスト環境構築は、これらの要素を総合的に判断して決定することが大切です。本記事が、皆さんのプロジェクトに最適なテスト環境を選択する助けになれば幸いです。
テストは開発の品質を支える重要な基盤ですから、適切なツール選択により、安心して開発を進められる環境を整えていきましょう。
関連リンク
各テストツールの公式ドキュメントやリソースをまとめました。より詳しい情報は、こちらをご参照ください。
公式ドキュメント
Testing Library
参考記事・リソース
articleテスト環境比較:Vitest vs Jest vs Playwright CT ― Vite プロジェクトの最適解
articleVite CSS HMR が反映されない時のチェックリスト:PostCSS/Modules/Cache 編
articleesbuild プリバンドルを理解する:Vite の optimizeDeps 深掘り
articleVite 本番の可観測性:ソースマップアップロードと Sentry 連携でエラーを特定
articleMicro Frontends 設計:`vite-plugin-federation` で分割可能な UI を構築
article【保存版】Vite 設定オプション早見表:`resolve` / `optimizeDeps` / `build` / `server`
articleWebSocket が「200 OK で Upgrade されない」原因と対処:プロキシ・ヘッダー・TLS の落とし穴
articleWebRTC 本番運用の SLO 設計:接続成功率・初画出し時間・通話継続率の基準値
articleAstro のレンダリング戦略を一望:MPA× 部分ハイドレーションの強みを図解解説
articleWebLLM が読み込めない時の原因と解決策:CORS・MIME・パス問題を総点検
articleVitest ESM/CJS 混在で `Cannot use import statement outside a module` が出る技術対処集
articleテスト環境比較:Vitest vs Jest vs Playwright CT ― Vite プロジェクトの最適解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来