3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vitest ブラウザモードと Playwright を比較する

3
Posted at

はじめに

この記事では、Vitest ブラウザモードと Playwright を比較し、それぞれの特徴と使い分けについて解説します。

どちらもブラウザ上でテストを実行できるツールですが、目的や得意分野が異なります。プロジェクトに適したツールを選択するための参考にしてください。

開発環境

開発環境は以下の通りです。

  • Windows 11
  • VSCode
  • Vite 7.3.1
  • React 19.2.3
  • TypeScript 5.9.3
  • Vitest 4.0.17
  • Playwright

両ツールの概要

Vitest ブラウザモード

Vitest は Vite ネイティブなテストフレームワークです。ブラウザモードを使用すると、jsdom などのシミュレーション環境ではなく、実際のブラウザ上でユニットテストやコンポーネントテストを実行できます。

主な用途はコンポーネントの単体テストです。

Playwright

Playwright は Microsoft が開発した E2E(End-to-End)テストフレームワークです。実際のブラウザを操作してアプリケーション全体の動作を検証します。

主な用途はアプリケーション全体の結合テストです。

セットアップの比較

Vitest ブラウザモードのセットアップ

簡単セットアップコマンドを使用すると、必要な依存関係のインストールとブラウザ設定を自動で行えます。

npx vitest init browser

手動でインストールする場合は以下のコマンドを実行します。

npm install -D vitest @vitest/browser-playwright vitest-browser-react

Playwright(E2E テスト)のセットアップ

npm init playwright@latest

ここでの Playwright は E2E テストフレームワークとしての利用です。Vitest ブラウザモードで使用する @vitest/browser-playwright とは別のパッケージです。

インストール時の質問に回答すると、設定ファイルが自動生成されます。
baseURLhttp://localhost:5173/ に変更します。

playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });

/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  testDir: "./src/__tests__/e2e",
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: "html",
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('')`. */
    baseURL: "http://localhost:5173", // 変更する

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: "on-first-retry",
  },

  /* Configure projects for major browsers */
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },

    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
    },

    {
      name: "webkit",
      use: { ...devices["Desktop Safari"] },
    },

    /* Test against mobile viewports. */
    // {
    //   name: 'Mobile Chrome',
    //   use: { ...devices['Pixel 5'] },
    // },
    // {
    //   name: 'Mobile Safari',
    //   use: { ...devices['iPhone 12'] },
    // },

    /* Test against branded browsers. */
    // {
    //   name: 'Microsoft Edge',
    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
    // },
    // {
    //   name: 'Google Chrome',
    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
    // },
  ],

  /* Run your local dev server before starting the tests */
  // webServer: {
  //   command: 'npm run start',
  //   url: 'http://localhost:3000',
  //   reuseExistingServer: !process.env.CI,
  // },
});

テスト対象のコンポーネント

比較のため、同じコンポーネントをテストします。

components/LoginForm.tsx
import { useState } from "react";

type LoginFormProps = {
  onSubmit: (email: string, password: string) => void;
};

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    if (!email) {
      setError("メールアドレスを入力してください");
      return;
    }
    if (!password) {
      setError("パスワードを入力してください");
      return;
    }

    setError("");
    onSubmit(email, password);
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>ログイン</h2>
      {error && (
        <p data-testid="error-message" role="alert">
          {error}
        </p>
      )}
      <div>
        <label htmlFor="email">メールアドレス</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="example@email.com"
        />
      </div>
      <div>
        <label htmlFor="password">パスワード</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="パスワード"
        />
      </div>
      <button type="submit">ログイン</button>
    </form>
  );
}

App.tsx
import { LoginForm } from "./components/LoginForm";

function App() {
  const handleLogin = (email: string, password: string) => {
    console.log("ログイン:", email, password);
    // 実際のアプリケーションではここで認証処理を行う
  };

  return <LoginForm onSubmit={handleLogin} />;
}

export default App;

vitest-browser-playwright-demo-Google-Chrome-2026-01-20-11-22-36.gif

テストコードの比較

Vitest ブラウザモードのテストコード

__tests__/components/LoginForm.test.tsx
import { expect, test, vi } from "vitest";
import { render } from "vitest-browser-react";
import { LoginForm } from "../../components/LoginForm";

test("フォームが正しく表示される", async () => {
  const screen = await render(<LoginForm onSubmit={vi.fn()} />);

  await expect
    .element(screen.getByRole("heading", { name: "ログイン" }))
    .toBeVisible();
  await expect.element(screen.getByLabelText("メールアドレス")).toBeVisible();
  await expect.element(screen.getByLabelText("パスワード")).toBeVisible();
  await expect
    .element(screen.getByRole("button", { name: "ログイン" }))
    .toBeVisible();
});

test("メールアドレス未入力でエラーが表示される", async () => {
  const screen = await render(<LoginForm onSubmit={vi.fn()} />);

  await screen.getByRole("button", { name: "ログイン" }).click();

  await expect
    .element(screen.getByTestId("error-message"))
    .toHaveTextContent("メールアドレスを入力してください");
});

test("パスワード未入力でエラーが表示される", async () => {
  const screen = await render(<LoginForm onSubmit={vi.fn()} />);

  await screen.getByLabelText("メールアドレス").fill("test@example.com");
  await screen.getByRole("button", { name: "ログイン" }).click();

  await expect
    .element(screen.getByTestId("error-message"))
    .toHaveTextContent("パスワードを入力してください");
});

test("正しい入力でonSubmitが呼ばれる", async () => {
  const handleSubmit = vi.fn();
  const screen = await render(<LoginForm onSubmit={handleSubmit} />);

  await screen.getByLabelText("メールアドレス").fill("test@example.com");
  await screen.getByLabelText("パスワード").fill("password123");
  await screen.getByRole("button", { name: "ログイン" }).click();

  expect(handleSubmit).toHaveBeenCalledWith("test@example.com", "password123");
});

Playwright のテストコード

__tests__/e2e/login.test.ts
import { test, expect } from "@playwright/test";

test("フォームが正しく表示される", async ({ page }) => {
  await page.goto("/login");

  await expect(page.getByRole("heading", { name: "ログイン" })).toBeVisible();
  await expect(page.getByLabel("メールアドレス")).toBeVisible();
  await expect(page.getByLabel("パスワード")).toBeVisible();
  await expect(page.getByRole("button", { name: "ログイン" })).toBeVisible();
});

test("メールアドレス未入力でエラーが表示される", async ({ page }) => {
  await page.goto("/login");

  await page.getByRole("button", { name: "ログイン" }).click();

  await expect(page.getByTestId("error-message")).toHaveText(
    "メールアドレスを入力してください",
  );
});

test("パスワード未入力でエラーが表示される", async ({ page }) => {
  await page.goto("/login");

  await page.getByLabel("メールアドレス").fill("test@example.com");
  await page.getByRole("button", { name: "ログイン" }).click();

  await expect(page.getByTestId("error-message")).toHaveText(
    "パスワードを入力してください",
  );
});

test("正しい入力でログインできる", async ({ page }) => {
  await page.goto("/login");

  await page.getByLabel("メールアドレス").fill("test@example.com");
  await page.getByLabel("パスワード").fill("password123");
  await page.getByRole("button", { name: "ログイン" }).click();

  // 実際のアプリケーションではログイン後の画面遷移を確認
  await expect(page).toHaveURL("/dashboard");
});

テストコードの主な違い

観点 Vitest ブラウザモード Playwright
レンダリング await render(<Component />) でコンポーネント単体 page.goto('/path') でページ全体
要素取得 screen.getByRole() page.getByRole()
アサーション expect.element() expect()
モック vi.fn() で関数をモック可能 API モックは page.route() を使用
テスト範囲 コンポーネント単位 ページ・フロー単位

テスト実行の比較

Vitest ブラウザモード

# テスト実行
npx vitest

# 単発実行
npx vitest run

# UI モード
npx vitest --ui
 ✓  chromium  src/__tests__/components/LoginForm.test.tsx (4 tests) 672ms
   ✓ フォームが正しく表示される 87ms
   ✓ メールアドレス未入力でエラーが表示される  339ms
   ✓ パスワード未入力でエラーが表示される 95ms
   ✓ 正しい入力でonSubmitが呼ばれる 149ms

 Test Files  1 passed (1)
      Tests  4 passed (4)
   Start at  11:27:24
   Duration  5.86s (transform 0ms, setup 0ms, import 136ms, tests 672ms, environment 0ms)

 PASS  Waiting for file changes...
       press h to show help, press q to quit

image.png

Playwright

# テスト実行
npx playwright test

# ヘッドありモード(ブラウザ表示)
npx playwright test --headed

# UI モード
npx playwright test --ui

# レポート表示
npx playwright show-report
Running 12 tests using 4 workers
  12 passed (18.1s)

To open last HTML report run:

  npx playwright show-report

image.png

機能比較表

機能 Vitest ブラウザモード Playwright
主な用途 コンポーネントテスト E2E テスト
テスト対象 コンポーネント単体 アプリケーション全体
実行速度 速い やや遅い
セットアップ Vite プロジェクトなら簡単 独立してセットアップ
HMR 対応 あり なし
スクリーンショット あり(Visual Regression Testing) 高機能(差分比較など)
トレース あり(Trace View) あり(操作の記録・再生)
ネットワークモック 限定的 高機能
複数ブラウザ 対応 対応
並列実行 対応 対応
CI 統合 容易 容易

Vitest ブラウザモード固有の機能

Vite との統合

Vite の HMR(Hot Module Replacement)が利用でき、テストファイルの変更が即座に反映されます。

Trace View

Vitest 4.x では Playwright の Trace 機能がサポートされ、テスト実行の詳細な記録を確認できます。

vite.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { playwright } from '@vitest/browser-playwright'

export default defineConfig({
  plugins: [react()],
  test: {
    browser: {
      enabled: true,
      provider: playwright(),
      instances: [{ browser: 'chromium' }],
      trace: 'retain-on-failure', // 失敗したテストのみトレースを保持
    },
  },
})

トレースファイルは __traces__ フォルダに保存され、Playwright Trace Viewer で確認できます。

npx playwright show-trace "path-to-trace-file.zip"

Visual Regression Testing

スクリーンショットベースのビジュアルリグレッションテストが可能です。

import { expect, test } from 'vitest'
import { render } from 'vitest-browser-react'
import { LoginForm } from '../LoginForm'

test('ログインフォームの見た目が正しい', async () => {
  const screen = await render(<LoginForm onSubmit={vi.fn()} />)

  // コンポーネントのスクリーンショットを撮影して比較
  await expect.element(screen.getByTestId('login-form')).toMatchScreenshot('login-form')
})

モジュールモック

vi.mock() でモジュールをモックできます。ブラウザモードでは { spy: true } オプションを使用することで、モジュールのエクスポートを置き換えずにスパイできます。

import { vi } from 'vitest'
import * as auth from '../api/auth'

// spy: true でモジュールをスパイ
vi.mock('../api/auth', { spy: true })

test('ログイン処理が呼ばれる', async () => {
  vi.mocked(auth.login).mockResolvedValue({ success: true })

  const screen = await render(<LoginForm />)
  // ...テスト処理

  expect(auth.login).toHaveBeenCalledWith('test@example.com', 'password123')
})

インラインスナップショット

テストファイル内にスナップショットを記述できます。

test('コンポーネントのスナップショット', async () => {
  const screen = await render(<LoginForm onSubmit={vi.fn()} />)
  
  expect(screen.container.innerHTML).toMatchInlineSnapshot()
})

Playwright 固有の機能

トレース機能

テスト実行の詳細な記録を取得できます。

playwright.config.ts
export default defineConfig({
  use: {
    trace: 'on-first-retry',
  },
})

スクリーンショット比較

ビジュアルリグレッションテストが可能です。

test('ページのスクリーンショット', async ({ page }) => {
  await page.goto('/login')
  await expect(page).toHaveScreenshot('login-page.png')
})

ネットワークモック

API レスポンスをモックできます。

test('API エラー時の表示', async ({ page }) => {
  await page.route('**/api/login', (route) => {
    route.fulfill({
      status: 401,
      body: JSON.stringify({ error: '認証エラー' }),
    })
  })

  await page.goto('/login')
  // エラー表示の確認
})

コードジェネレーター

ブラウザ操作からテストコードを自動生成できます。

npx playwright codegen http://localhost:5173

使い分けの指針

Vitest ブラウザモードを選ぶケース

  • コンポーネント単体の動作確認
  • Props や State の変化に応じた表示確認
  • イベントハンドラの呼び出し確認
  • フォームのバリデーションロジック確認
  • 高速なフィードバックが必要な場合

Playwright を選ぶケース

  • ページ間の遷移を含むフロー確認
  • 認証フロー全体のテスト
  • API との統合テスト
  • 複数ブラウザでの互換性確認
  • ビジュアルリグレッションテスト
  • 本番環境に近い条件でのテスト

推奨されるテスト戦略

テストピラミッドの考え方に基づき、両ツールを組み合わせて使用することを推奨します。

        /\
       /  \
      / E2E \        ← Playwright(少数の重要なフロー)
     /--------\
    /   統合   \      ← Vitest ブラウザモード(コンポーネント連携)
   /--------------\
  /   ユニット    \    ← Vitest Node.js モード(ロジック)
 /------------------\

具体的な配分の目安は以下の通りです。

  • ユニットテスト(Vitest Node.js): 70% - ビジネスロジック、ユーティリティ関数
  • コンポーネントテスト(Vitest ブラウザ): 20% - UI コンポーネントの動作
  • E2E テスト(Playwright): 10% - 重要なユーザーフロー

package.json のスクリプト例

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "test": "vitest",
    "test:unit": "vitest --project unit",
    "test:browser": "vitest --project browser",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:all": "npm run test:unit && npm run test:browser && npm run test:e2e"
  }
}

まとめ

この記事では、Vitest ブラウザモードと Playwright の違いについて解説しました。

Vitest ブラウザモードは、実際のブラウザ上でコンポーネント単体をテストするためのツールです。Vite との統合により高速な HMR が利用でき、vi.mock() によるモジュールモックも可能です。Vitest 4.x では Trace View や Visual Regression Testing も追加され、デバッグ機能が強化されています。コンポーネントの Props や State の変化、イベントハンドラの動作確認に適しています。

Playwright は、アプリケーション全体を対象とした E2E テストフレームワークです。ページ遷移を含むユーザーフローのテスト、トレース機能、スクリーンショット比較、ネットワークモックなど、本番環境に近い条件でのテストに強みがあります。

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?