Playwright + AI: 自動化されたE2Eテスト作成

Playwright + AI: 自動化されたE2Eテスト作成

PlaywrightとAI Codegenを活用したE2Eテスト自動化。TypeScript実践、GitHub Actions統合、ビジュアルリグレッションテストまで、実務で即活用できる完全ガイド

概要

現代のウェブ開発において、E2E(End-to-End)テストは品質保証の要となっています。しかし、従来のテスト作成は時間がかかり、メンテナンスコストも高いという課題がありました。

PlaywrightとAI技術の組み合わせは、この状況を一変させます。本記事では、MicrosoftのPlaywrightフレームワークとAIベースのコード生成ツールを活用し、効率的で保守性の高いE2Eテスト環境を構築する方法を詳しく解説します。

この記事で学べること:

  • PlaywrightとAI Codegenの基本概念
  • TypeScriptを使った実践的なテスト実装
  • GitHub ActionsによるCI/CD統合
  • ビジュアルリグレッションテストの導入
  • 実務で即活用できるベストプラクティス

Playwrightの核心概念

Playwrightとは

PlaywrightはMicrosoftが開発したモダンなE2Eテスティングフレームワークです。Chromium、Firefox、WebKitの3つのブラウザエンジンをサポートし、クロスブラウザテストを簡単に実装できます。

主な特徴:

  • 自動待機機能: 要素が準備できるまで自動的に待機
  • 高速実行: 並列テスト実行とヘッドレスモードによる高速化
  • 信頼性: ネットワーク傍受、モック機能による安定したテスト
  • 開発者体験: TypeScript完全サポート、優れたデバッグツール

アーキテクチャ概要

graph TB
    A[テストスクリプト] --> B[Playwright API]
    B --> C[Browser Context]
    C --> D[Chromium]
    C --> E[Firefox]
    C --> F[WebKit]
    D --> G[テスト対象アプリ]
    E --> G
    F --> G

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style G fill:#e8f5e9

Playwrightは独自のプロトコルを使用してブラウザを制御し、各ブラウザエンジンとの通信を最適化しています。この設計により、従来のSeleniumよりも高速で信頼性の高いテストが可能になります。

AIベースのテスト生成

Playwright Codegen - 自動コード生成

Playwright Codegenは、ブラウザ操作を記録し、自動的にテストコードを生成するツールです。

基本的な使い方:

# Codegenの起動
npx playwright codegen https://example.com

# 特定のブラウザで記録
npx playwright codegen --browser=firefox https://example.com

# デバイスエミュレーション
npx playwright codegen --device="iPhone 13" https://example.com

生成されるコードの例:

import { test, expect } from '@playwright/test';

test('ユーザーログインフロー', async ({ page }) => {
  // ページに移動
  await page.goto('https://example.com/login');

  // フォーム入力
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');

  // ログインボタンをクリック
  await page.click('button[type="submit"]');

  // ログイン成功を確認
  await expect(page).toHaveURL('https://example.com/dashboard');
  await expect(page.locator('h1')).toContainText('ダッシュボード');
});

Claude Code + Playwright MCP統合

Claude Code MCPサーバーは、Playwrightの機能をAIエージェントに統合し、自然言語でテストを作成できるようにします。

MCPサーバーの主要機能:

graph LR
    A[自然言語指示] --> B[Claude MCP]
    B --> C[ブラウザナビゲーション]
    B --> D[要素インタラクション]
    B --> E[スナップショット取得]
    B --> F[スクリーンショット]

    C --> G[テストコード生成]
    D --> G
    E --> G
    F --> G

    style A fill:#e1f5ff
    style B fill:#fff4e1
    style G fill:#e8f5e9

MCP設定例 (claude_desktop_config.json):

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": [
        "-y",
        "@executeautomation/playwright-mcp-server"
      ]
    }
  }
}

AI支援によるテスト作成の利点

  1. 学習曲線の短縮: セレクタ構文やAPIを完全に理解していなくても開始可能
  2. ベストプラクティスの自動適用: AIが推奨パターンを提案
  3. 迅速なプロトタイピング: アイデアから実装までの時間を大幅短縮
  4. コードレビュー支援: 生成されたコードの改善点を提案

実践実装ガイド

Step 1: プロジェクトセットアップ

# 新規プロジェクト作成
mkdir my-e2e-tests
cd my-e2e-tests

# package.json初期化
npm init -y

# Playwrightインストール
npm install -D @playwright/test

# ブラウザインストール
npx playwright install

Step 2: 基本設定

playwright.config.tsを作成:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // テストディレクトリ
  testDir: './tests',

  // タイムアウト設定
  timeout: 30 * 1000,
  expect: {
    timeout: 5000
  },

  // 並列実行設定
  fullyParallel: true,

  // 失敗時の再試行
  retries: process.env.CI ? 2 : 0,

  // ワーカー数
  workers: process.env.CI ? 1 : undefined,

  // レポーター
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }],
    ['junit', { outputFile: 'test-results.xml' }]
  ],

  // 共通設定
  use: {
    // ベースURL
    baseURL: 'http://localhost:3000',

    // トレース記録
    trace: 'on-first-retry',

    // スクリーンショット
    screenshot: 'only-on-failure',

    // ビデオ記録
    video: 'retain-on-failure',
  },

  // プロジェクト設定
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // モバイルテスト
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],

  // 開発サーバー起動
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Step 3: 実践的なテストケース

ログインフロー完全版

import { test, expect } from '@playwright/test';

test.describe('ユーザー認証フロー', () => {
  // 各テスト前の共通処理
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('正常なログインが成功する', async ({ page }) => {
    // フォーム入力
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'SecurePass123!');

    // ログインボタンクリック
    await page.click('[data-testid="login-button"]');

    // リダイレクト待機
    await page.waitForURL('/dashboard');

    // ダッシュボード要素の確認
    await expect(page.locator('h1')).toContainText('ダッシュボード');

    // ユーザー情報の表示確認
    await expect(page.locator('[data-testid="user-profile"]'))
      .toContainText('test@example.com');
  });

  test('無効な認証情報でエラーメッセージが表示される', async ({ page }) => {
    await page.fill('[data-testid="email-input"]', 'wrong@example.com');
    await page.fill('[data-testid="password-input"]', 'wrongpassword');

    await page.click('[data-testid="login-button"]');

    // エラーメッセージの確認
    const errorMessage = page.locator('[data-testid="error-message"]');
    await expect(errorMessage).toBeVisible();
    await expect(errorMessage).toContainText('メールアドレスまたはパスワードが正しくありません');
  });

  test('パスワード表示トグルが機能する', async ({ page }) => {
    const passwordInput = page.locator('[data-testid="password-input"]');
    const toggleButton = page.locator('[data-testid="password-toggle"]');

    // 初期状態: パスワード非表示
    await expect(passwordInput).toHaveAttribute('type', 'password');

    // トグルクリック: 表示
    await toggleButton.click();
    await expect(passwordInput).toHaveAttribute('type', 'text');

    // 再度クリック: 非表示
    await toggleButton.click();
    await expect(passwordInput).toHaveAttribute('type', 'password');
  });
});

ショッピングカート操作

test.describe('ショッピングカート機能', () => {
  test('商品の追加から購入完了まで', async ({ page }) => {
    await page.goto('/products');

    // 商品選択
    const firstProduct = page.locator('[data-testid="product-card"]').first();
    await firstProduct.click();

    // 商品詳細ページ
    await expect(page).toHaveURL(/\/product\/\d+/);

    // カート追加
    await page.click('[data-testid="add-to-cart"]');

    // カートバッジの更新確認
    await expect(page.locator('[data-testid="cart-badge"]'))
      .toContainText('1');

    // カートページへ移動
    await page.click('[data-testid="cart-icon"]');
    await expect(page).toHaveURL('/cart');

    // 商品がカートに存在することを確認
    const cartItems = page.locator('[data-testid="cart-item"]');
    await expect(cartItems).toHaveCount(1);

    // 数量変更
    await page.click('[data-testid="increase-quantity"]');
    await expect(page.locator('[data-testid="item-quantity"]'))
      .toContainText('2');

    // 合計金額の確認
    const totalPrice = page.locator('[data-testid="total-price"]');
    await expect(totalPrice).toBeVisible();

    // チェックアウト
    await page.click('[data-testid="checkout-button"]');
    await expect(page).toHaveURL('/checkout');
  });

  test('空のカートで警告メッセージが表示される', async ({ page }) => {
    await page.goto('/cart');

    const emptyMessage = page.locator('[data-testid="empty-cart-message"]');
    await expect(emptyMessage).toBeVisible();
    await expect(emptyMessage).toContainText('カートは空です');

    // チェックアウトボタンが無効化されている
    await expect(page.locator('[data-testid="checkout-button"]'))
      .toBeDisabled();
  });
});

フォームバリデーション

test.describe('お問い合わせフォーム', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/contact');
  });

  test('必須フィールドのバリデーション', async ({ page }) => {
    // 空の状態で送信
    await page.click('[data-testid="submit-button"]');

    // 各フィールドのエラーメッセージ確認
    await expect(page.locator('[data-testid="name-error"]'))
      .toContainText('名前を入力してください');
    await expect(page.locator('[data-testid="email-error"]'))
      .toContainText('メールアドレスを入力してください');
    await expect(page.locator('[data-testid="message-error"]'))
      .toContainText('メッセージを入力してください');
  });

  test('メールアドレス形式のバリデーション', async ({ page }) => {
    await page.fill('[data-testid="email-input"]', 'invalid-email');
    await page.click('[data-testid="submit-button"]');

    await expect(page.locator('[data-testid="email-error"]'))
      .toContainText('有効なメールアドレスを入力してください');
  });

  test('正常な送信が成功する', async ({ page }) => {
    // フォーム入力
    await page.fill('[data-testid="name-input"]', '山田太郎');
    await page.fill('[data-testid="email-input"]', 'yamada@example.com');
    await page.fill('[data-testid="message-input"]', 'お問い合わせ内容です。');

    // 送信
    await page.click('[data-testid="submit-button"]');

    // 成功メッセージの確認
    await expect(page.locator('[data-testid="success-message"]'))
      .toContainText('送信が完了しました');
  });
});

Step 4: Page Object Model(POM)パターン

再利用性と保守性を向上させるため、Page Object Modelを導入します。

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.locator('[data-testid="email-input"]');
    this.passwordInput = page.locator('[data-testid="password-input"]');
    this.loginButton = page.locator('[data-testid="login-button"]');
    this.errorMessage = page.locator('[data-testid="error-message"]');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async getErrorMessage(): Promise<string> {
    return await this.errorMessage.textContent() || '';
  }
}

POMを使用したテスト:

import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

test('POMパターンを使用したログインテスト', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('test@example.com', 'SecurePass123!');

  await expect(page).toHaveURL('/dashboard');
});

GitHub Actions CI/CD統合

ワークフロー設定

.github/workflows/playwright.ymlを作成:

name: Playwright Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]

    steps:
    - name: リポジトリのチェックアウト
      uses: actions/checkout@v4

    - name: Node.jsのセットアップ
      uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'npm'

    - name: 依存関係のインストール
      run: npm ci

    - name: Playwrightブラウザのインストール
      run: npx playwright install --with-deps ${{ matrix.browser }}

    - name: テスト実行
      run: npx playwright test --project=${{ matrix.browser }}
      env:
        CI: true

    - name: テストレポートのアップロード
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report-${{ matrix.browser }}
        path: playwright-report/
        retention-days: 30

    - name: テスト結果のアップロード
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: test-results-${{ matrix.browser }}
        path: test-results/
        retention-days: 30

  report:
    name: レポート生成
    runs-on: ubuntu-latest
    needs: test
    if: always()

    steps:
    - name: テストレポートのダウンロード
      uses: actions/download-artifact@v4
      with:
        path: all-reports

    - name: 統合レポート生成
      run: |
        echo "すべてのテストが完了しました"
        # ここにレポート統合ロジックを追加可能

環境別設定

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: process.env.CI
      ? 'https://staging.example.com'
      : 'http://localhost:3000',

    // CI環境ではヘッドレスモード強制
    headless: !!process.env.CI,
  },

  // CI環境では並列実行を抑制
  workers: process.env.CI ? 1 : undefined,

  // CI環境では再試行回数を増やす
  retries: process.env.CI ? 2 : 0,
});

ビジュアルリグレッションテスト

スクリーンショット比較

Playwrightの組み込み機能を使用したビジュアルテスト:

test('ホームページのビジュアルリグレッション', async ({ page }) => {
  await page.goto('/');

  // ページ全体のスクリーンショット比較
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixels: 100,
  });
});

test('ボタンコンポーネントのビジュアル確認', async ({ page }) => {
  await page.goto('/components');

  const button = page.locator('[data-testid="primary-button"]');

  // 要素のスクリーンショット比較
  await expect(button).toHaveScreenshot('primary-button.png');

  // ホバー状態
  await button.hover();
  await expect(button).toHaveScreenshot('primary-button-hover.png');
});

Percy統合(オプション)

より高度なビジュアルテストには、Percyなどのサービスを統合できます:

npm install --save-dev @percy/cli @percy/playwright
import { test } from '@playwright/test';
import percySnapshot from '@percy/playwright';

test('Percyビジュアルテスト', async ({ page }) => {
  await page.goto('/');

  // Percyにスナップショット送信
  await percySnapshot(page, 'Homepage');

  // レスポンシブテスト
  await page.setViewportSize({ width: 375, height: 667 });
  await percySnapshot(page, 'Homepage - Mobile');
});

ベストプラクティスと注意事項

セレクタ戦略

推奨される優先順位:

  1. data-testid属性: page.locator('[data-testid="submit-button"]')
  2. role属性: page.getByRole('button', { name: '送信' })
  3. ラベルテキスト: page.getByLabel('メールアドレス')
  4. プレースホルダー: page.getByPlaceholder('名前を入力')
  5. CSS/XPath: 最終手段として使用

アンチパターン:

// 避けるべき: 脆弱なCSSセレクタ
await page.click('.btn.btn-primary.mt-4');

// 推奨: 明示的なdata属性
await page.click('[data-testid="submit-button"]');

待機戦略

Playwrightの自動待機を活用:

// 自動待機(推奨)
await page.click('button'); // 要素が準備完了まで自動待機

// 明示的待機(必要な場合のみ)
await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
await page.waitForLoadState('networkidle');

// タイムアウトカスタマイズ
await page.click('button', { timeout: 10000 });

エラーハンドリング

test('エラーハンドリングの例', async ({ page }) => {
  // ソフトアサーション(テスト継続)
  await expect.soft(page.locator('h1')).toContainText('期待値');
  await expect.soft(page.locator('p')).toBeVisible();

  // try-catchでカスタムエラー処理
  try {
    await page.click('[data-testid="optional-button"]', { timeout: 3000 });
  } catch (error) {
    console.log('オプショナル要素が見つかりませんでした');
  }

  // 条件付きアサーション
  const element = page.locator('[data-testid="dynamic-element"]');
  if (await element.isVisible()) {
    await expect(element).toHaveText('動的コンテンツ');
  }
});

テストデータ管理

// fixtures/testData.ts
export const testUsers = {
  admin: {
    email: 'admin@example.com',
    password: 'AdminPass123!',
  },
  regularUser: {
    email: 'user@example.com',
    password: 'UserPass123!',
  },
};

// テストで使用
import { testUsers } from './fixtures/testData';

test('管理者ログイン', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login(testUsers.admin.email, testUsers.admin.password);
});

パフォーマンス最適化

test.describe('パフォーマンステスト', () => {
  test('ページロード時間の測定', async ({ page }) => {
    const startTime = Date.now();

    await page.goto('/');

    const loadTime = Date.now() - startTime;
    expect(loadTime).toBeLessThan(3000); // 3秒以内
  });

  test('Lighthouse統合', async ({ page }) => {
    await page.goto('/');

    // Playwrightのパフォーマンスメトリクス
    const metrics = await page.evaluate(() => {
      const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
      return {
        domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart,
        loadComplete: navigation.loadEventEnd - navigation.fetchStart,
      };
    });

    expect(metrics.domContentLoaded).toBeLessThan(2000);
  });
});

デバッグテクニック

test('デバッグ支援機能', async ({ page }) => {
  // ステップバイステップ実行
  await page.pause();

  // コンソールログの取得
  page.on('console', msg => console.log('ブラウザコンソール:', msg.text()));

  // スクリーンショット保存
  await page.screenshot({ path: 'debug-screenshot.png' });

  // トレース記録
  await page.context().tracing.start({ screenshots: true, snapshots: true });
  // ... テストアクション
  await page.context().tracing.stop({ path: 'trace.zip' });
});

結論

Playwright + AIの組み合わせは、E2Eテスト自動化における革新的なアプローチです。本記事で紹介した内容をまとめます:

主要なポイント:

  1. Playwright Codegen: ブラウザ操作から自動的にテストコード生成
  2. Claude MCP統合: 自然言語でテストを記述可能
  3. TypeScript完全サポート: 型安全性と優れた開発者体験
  4. CI/CD統合: GitHub Actionsで自動テスト実行
  5. ビジュアルリグレッション: UIの意図しない変更を検出

次のステップ:

  • 既存プロジェクトにPlaywrightを導入
  • POMパターンでテストの保守性を向上
  • CI/CDパイプラインに統合
  • ビジュアルテストで品質をさらに強化

E2Eテストは初期投資が必要ですが、長期的には開発速度の向上とバグ削減につながります。Playwrightの強力な機能とAI支援を活用し、効率的なテスト自動化を実現しましょう。

参考資料

公式ドキュメント

MCPとAI統合

コミュニティリソース

関連技術


著者より: E2Eテスト自動化は継続的な改善プロセスです。小さく始めて、徐々にカバレッジを拡大していくことをお勧めします。質問やフィードバックがあれば、コメント欄でお気軽にお知らせください。

他の言語で読む

この記事は役に立ちましたか?

より良いコンテンツを作成するための力になります。コーヒー一杯で応援してください!☕

著者について

JK

Kim Jangwook

AI/LLM専門フルスタック開発者

10年以上のWeb開発経験を活かし、AIエージェントシステム、LLMアプリケーション、自動化ソリューションを構築しています。Claude Code、MCP、RAGシステムの実践的な知見を共有します。