テストツール

Playwright

概要

PlaywrightはMicrosoft社開発のクロスブラウザ自動化ライブラリで、Chromium、Firefox、WebKitをサポートする最新のE2Eテストフレームワークです。真のクロスブラウザテスト、高速で安定したテスト実行、豊富なAPI機能により、現代のWebアプリケーション開発における信頼性の高いインテグレーションテストとE2Eテストを実現します。JavaScript、TypeScript、Python、C#、Javaの多言語サポートと、ヘッドレス・フル表示モード、モバイルエミュレーション、並列実行機能により、あらゆる開発チームのテスト要件に対応した包括的なブラウザ自動化ソリューションを提供しています。

詳細

Playwright 2025年版は、Webアプリケーションテストにおける最先端技術として確固たる地位を築いています。Microsoft社のブラウザエンジン開発チームが直接開発し、Chromium(Google Chrome、Microsoft Edge)、Firefox、WebKit(Safari)の3大ブラウザエンジンを完全サポート。従来のSeleniumベースのツールと比較して10倍以上の高速化を実現し、テストの信頼性と安定性を大幅に向上させています。自動待機(auto-wait)機能、要素の完全な可視化確認、ネットワークレスポンスの詳細制御、リクエスト・レスポンスのインターセプト機能により、フラキーテストを根本的に解決。API Testing、Visual Testing、コンポーネントテスト機能も統合され、E2Eテストからユニットテストまでワンストップで対応可能です。DevOpsパイプラインとの統合に優れ、CI/CD環境でのヘッドレス実行、詳細なテストレポート生成、スクリーンショット・動画記録機能により、継続的なテスト自動化を強力に支援します。

主な特徴

  • 真のクロスブラウザ対応: Chromium、Firefox、WebKitの3エンジン完全サポート
  • 高速で安定したテスト実行: 自動待機とスマートな要素検出による信頼性の向上
  • 多言語サポート: JavaScript/TypeScript、Python、C#、Java対応
  • API Testing統合: UIテストとAPIテスト一体型のワークフロー
  • ビジュアルテスト: スクリーンショット比較とリグレッション検出
  • 並列実行とCI/CD統合: 高速テスト実行と継続的インテグレーション最適化

メリット・デメリット

メリット

  • Microsoft社開発による高い技術力と継続的なブラウザエンジン更新
  • 自動待機機能によるフラキーテストの大幅削減と安定したテスト実行
  • 3つのブラウザエンジン完全対応による真のクロスブラウザテスト
  • JavaScript、Python、C#、Javaの多言語サポートと豊富なエコシステム
  • API Testing、Visual Testing、Component Testingの統合ソリューション
  • 並列実行とCI/CD最適化による高速テスト実行とDevOps統合
  • 詳細なテストレポート、スクリーンショット、動画記録機能
  • 優れたデバッグ機能とリアルタイムテスト監視

デメリット

  • 比較的新しいツールのため、Seleniumと比較してコミュニティが小さい
  • 3つのブラウザエンジンインストールによるディスク容量とメモリ使用量増加
  • 古いブラウザバージョンや特殊なブラウザ(IE等)は非対応
  • Seleniumと比較してサードパーティ拡張機能やプラグインが少ない
  • 学習コストと既存Seleniumテストからの移行コスト
  • 一部の複雑なSPA(Single Page Application)で設定調整が必要

参考ページ

書き方の例

インストールとセットアップ

# JavaScript/TypeScript版のインストール
npm init playwright@latest
# または
npm install -D @playwright/test
npx playwright install

# Python版のインストール
pip install playwright
playwright install

# Java版のセットアップ (Maven)
# pom.xmlに依存関係を追加
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.47.0</version>
</dependency>

# C#版のインストール (.NET)
dotnet add package Microsoft.Playwright
dotnet tool install --global Microsoft.Playwright.CLI
playwright install

# プロジェクト初期化
npx playwright init
playwright codegen https://example.com  # テストコード自動生成

基本的なE2Eテスト(JavaScript/TypeScript)

// tests/example.spec.js
import { test, expect } from '@playwright/test';

test('基本的なページナビゲーションテスト', async ({ page }) => {
  // ページに移動
  await page.goto('https://example.com');
  
  // ページタイトルの確認
  await expect(page).toHaveTitle(/Example Domain/);
  
  // 要素の表示確認
  await expect(page.locator('h1')).toHaveText('Example Domain');
  await expect(page.locator('h1')).toBeVisible();
});

test('フォーム入力とナビゲーション', async ({ page }) => {
  await page.goto('https://example.com/contact');
  
  // フォーム入力
  await page.fill('input[name="name"]', '田中太郎');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('textarea[name="message"]', 'お問い合わせ内容です。');
  
  // チェックボックスとラジオボタン
  await page.check('input[type="checkbox"]');
  await page.click('input[value="option1"]');
  
  // 送信ボタンクリック
  await page.click('button[type="submit"]');
  
  // 成功メッセージの確認
  await expect(page.locator('.success-message')).toHaveText('送信完了しました');
  
  // URLの変更確認
  await expect(page).toHaveURL(/.*\/contact\/success/);
});

test('複数要素のテストとアサーション', async ({ page }) => {
  await page.goto('https://example.com/products');
  
  // 複数要素の存在確認
  const products = page.locator('.product-item');
  await expect(products).toHaveCount(8);
  
  // 各要素のテキスト確認
  await expect(products.nth(0)).toContainText('商品1');
  await expect(products.nth(1)).toContainText('商品2');
  
  // 条件付きアサーション
  const cartButton = page.locator('.add-to-cart');
  if (await cartButton.isVisible()) {
    await cartButton.click();
    await expect(page.locator('.cart-count')).toHaveText('1');
  }
});

並列テストと設定(playwright.config.js)

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true, // 並列実行有効化
  forbidOnly: !!process.env.CI, // CI環境でtest.onlyを禁止
  retries: process.env.CI ? 2 : 0, // CI環境でのリトライ設定
  workers: process.env.CI ? 1 : undefined, // CI環境での並列度調整
  
  // レポート設定
  reporter: [
    ['html'], 
    ['junit', { outputFile: 'test-results/junit.xml' }],
    ['json', { outputFile: 'test-results/results.json' }]
  ],
  
  use: {
    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'] },
    },
    {
      name: 'Microsoft Edge',
      use: { ...devices['Desktop Edge'], channel: 'msedge' },
    },
  ],

  // 開発サーバーの自動起動
  webServer: {
    command: 'npm run start',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

API Testing統合

// tests/api.spec.js
import { test, expect } from '@playwright/test';

test.describe('API Testing', () => {
  let apiContext;

  test.beforeAll(async ({ playwright }) => {
    // API用のコンテキスト作成
    apiContext = await playwright.request.newContext({
      baseURL: 'https://api.example.com',
      extraHTTPHeaders: {
        'Authorization': `Bearer ${process.env.API_TOKEN}`,
        'Content-Type': 'application/json',
      },
    });
  });

  test.afterAll(async () => {
    await apiContext.dispose();
  });

  test('GETリクエストのテスト', async () => {
    const response = await apiContext.get('/users/123');
    
    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(200);
    
    const user = await response.json();
    expect(user).toMatchObject({
      id: 123,
      name: expect.any(String),
      email: expect.stringContaining('@'),
    });
  });

  test('POSTリクエストとデータ作成', async () => {
    const newUser = {
      name: '新規ユーザー',
      email: '[email protected]',
      role: 'user'
    };

    const response = await apiContext.post('/users', {
      data: newUser
    });

    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(201);

    const createdUser = await response.json();
    expect(createdUser).toMatchObject(newUser);
    expect(createdUser.id).toBeDefined();
  });

  test('UI操作とAPI検証の組み合わせ', async ({ page }) => {
    // UIで新しいアイテムを作成
    await page.goto('/dashboard');
    await page.fill('input[name="title"]', 'New Item');
    await page.click('button[type="submit"]');

    // 作成成功メッセージの確認
    await expect(page.locator('.success')).toBeVisible();

    // APIで作成されたアイテムを検証
    const response = await apiContext.get('/items');
    const items = await response.json();
    const createdItem = items.find(item => item.title === 'New Item');
    
    expect(createdItem).toBeDefined();
    expect(createdItem.status).toBe('active');
  });
});

Python版の例

# test_example.py
import pytest
from playwright.sync_api import Page, expect

def test_basic_navigation(page: Page):
    """基本的なページナビゲーションテスト"""
    page.goto("https://example.com")
    
    # ページタイトルの確認
    expect(page).to_have_title("Example Domain")
    
    # 要素の表示確認
    heading = page.locator("h1")
    expect(heading).to_have_text("Example Domain")
    expect(heading).to_be_visible()

def test_form_interaction(page: Page):
    """フォーム操作のテスト"""
    page.goto("https://example.com/contact")
    
    # フォーム入力
    page.fill('input[name="name"]', '田中太郎')
    page.fill('input[name="email"]', '[email protected]')
    page.fill('textarea[name="message"]', 'お問い合わせ内容です。')
    
    # 送信ボタンクリック
    page.click('button[type="submit"]')
    
    # 成功メッセージの確認
    success_message = page.locator('.success-message')
    expect(success_message).to_have_text('送信完了しました')

def test_multiple_browsers(browser_name, page: Page):
    """マルチブラウザテスト"""
    page.goto("https://example.com")
    
    print(f"Testing on {browser_name}")
    
    # ブラウザ固有の動作確認
    if browser_name == "webkit":
        # Safari固有のテスト
        expect(page.locator("h1")).to_be_visible()
    elif browser_name == "firefox":
        # Firefox固有のテスト
        expect(page.locator("h1")).to_be_visible()
    else:
        # Chromium固有のテスト
        expect(page.locator("h1")).to_be_visible()

# conftest.py - テスト設定
import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session", params=["chromium", "firefox", "webkit"])
def browser_name(request):
    return request.param

@pytest.fixture(scope="session")
def browser(browser_name):
    with sync_playwright() as p:
        browser_type = getattr(p, browser_name)
        browser = browser_type.launch(headless=True)
        yield browser
        browser.close()

@pytest.fixture
def page(browser):
    context = browser.new_context()
    page = context.new_page()
    yield page
    context.close()

Java版の例

// ExampleTest.java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;

public class ExampleTest {
    static Playwright playwright;
    static Browser browser;
    BrowserContext context;
    Page page;

    @BeforeAll
    static void launchBrowser() {
        playwright = Playwright.create();
        browser = playwright.chromium().launch();
    }

    @AfterAll
    static void closeBrowser() {
        playwright.close();
    }

    @BeforeEach
    void createContextAndPage() {
        context = browser.newContext();
        page = context.newPage();
    }

    @AfterEach
    void closeContext() {
        context.close();
    }

    @Test
    void basicNavigationTest() {
        page.navigate("https://example.com");
        
        // ページタイトルの確認
        assertThat(page).hasTitle("Example Domain");
        
        // 要素の表示確認
        Locator heading = page.locator("h1");
        assertThat(heading).hasText("Example Domain");
        assertThat(heading).isVisible();
    }

    @Test
    void formInteractionTest() {
        page.navigate("https://example.com/contact");
        
        // フォーム入力
        page.fill("input[name='name']", "田中太郎");
        page.fill("input[name='email']", "[email protected]");
        page.fill("textarea[name='message']", "お問い合わせ内容です。");
        
        // 送信ボタンクリック
        page.click("button[type='submit']");
        
        // 成功メッセージの確認
        Locator successMessage = page.locator(".success-message");
        assertThat(successMessage).hasText("送信完了しました");
    }

    @Test
    void apiIntegrationTest() {
        // API Requestコンテキストの作成
        APIRequestContext request = playwright.request().newContext(
            new APIRequest.NewContextOptions()
                .setBaseURL("https://api.example.com")
                .setExtraHTTPHeaders(Map.of(
                    "Authorization", "Bearer " + System.getenv("API_TOKEN"),
                    "Content-Type", "application/json"
                ))
        );

        // GETリクエストのテスト
        APIResponse response = request.get("/users/123");
        assertTrue(response.ok());
        assertEquals(200, response.status());

        // POSTリクエストのテスト
        Map<String, Object> userData = Map.of(
            "name", "新規ユーザー",
            "email", "[email protected]"
        );
        
        APIResponse createResponse = request.post("/users", 
            RequestOptions.create().setData(userData));
        assertTrue(createResponse.ok());
        assertEquals(201, createResponse.status());

        request.dispose();
    }
}

Visual Testing(スクリーンショット比較)

// tests/visual.spec.js
import { test, expect } from '@playwright/test';

test('ビジュアルリグレッションテスト', async ({ page }) => {
  await page.goto('https://example.com');
  
  // ページ全体のスクリーンショット比較
  await expect(page).toHaveScreenshot('homepage.png');
  
  // 特定要素のスクリーンショット比較
  await expect(page.locator('.header')).toHaveScreenshot('header.png');
  
  // フルページスクリーンショット
  await expect(page).toHaveScreenshot('homepage-full.png', { fullPage: true });
});

test('レスポンシブデザインのテスト', async ({ page }) => {
  // モバイルビューポート
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('mobile-view.png');
  
  // タブレットビューポート
  await page.setViewportSize({ width: 768, height: 1024 });
  await expect(page).toHaveScreenshot('tablet-view.png');
  
  // デスクトップビューポート
  await page.setViewportSize({ width: 1920, height: 1080 });
  await expect(page).toHaveScreenshot('desktop-view.png');
});

test('動的コンテンツのビジュアルテスト', async ({ page }) => {
  await page.goto('https://example.com/chart');
  
  // アニメーション完了まで待機
  await page.waitForFunction('() => document.querySelector(".chart").classList.contains("loaded")');
  
  // マスキング(除外領域)を指定してスクリーンショット
  await expect(page).toHaveScreenshot('chart-view.png', {
    mask: [page.locator('.timestamp'), page.locator('.random-id')]
  });
});

CI/CD統合(GitHub Actions)

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

デバッグとトラブルシューティング

// tests/debug.spec.js
import { test, expect } from '@playwright/test';

test('デバッグ機能の活用', async ({ page }) => {
  // ブラウザ表示でデバッグ実行: npx playwright test --headed
  // デバッグモード: npx playwright test --debug
  
  await page.goto('https://example.com');
  
  // ステップバイステップ実行
  await page.pause(); // 実行一時停止
  
  // 要素の詳細情報出力
  const element = page.locator('h1');
  console.log('Element text:', await element.textContent());
  console.log('Element HTML:', await element.innerHTML());
  
  // ページ情報の出力
  console.log('Current URL:', page.url());
  console.log('Page title:', await page.title());
  
  // カスタム待機とタイムアウト
  await page.waitForTimeout(2000); // 2秒待機
  await page.waitForLoadState('networkidle'); // ネットワーク待機
  
  // トレース記録(詳細なデバッグ情報)
  await page.context().tracing.start({ screenshots: true, snapshots: true });
  // テスト実行
  await page.click('button');
  await page.context().tracing.stop({ path: 'trace.zip' });
  
  expect(await element.textContent()).toBe('Expected Text');
});

// コードジェネレーター使用
// npx playwright codegen https://example.com