この記事で分かること
- E2Eテストとは何か、なぜ自動化すべきなのか
- Playwrightのセットアップから最初のテスト実行まで
- Page Object Modelパターンで保守しやすいテストを書く方法
- GitHub ActionsにPlaywrightテストを組み込む具体的な手順
- テストが不安定になる原因と対処法
はじめに
「リリース前に画面を全部手でポチポチ確認して……」「確認漏れで本番障害が出て……」
こんな経験はありませんか?手動テストは確実そうに見えて、実は担当者の体力と集中力に依存した再現性のない作業です。機能が増えるたびに確認項目は膨らみ、やがて「全画面を毎回チェックするのは無理」という状態に陥ります。これが「手動テスト地獄」です。
この記事では、Playwright(プレイライト)というブラウザ自動操作ツールを使って、手動テストを自動化し、さらにCI(継続的インテグレーション)に組み込んで「コードを変更するたびに自動でテストが走る」状態を構築する方法をハンズオン形式で解説します。
前提知識
- E2Eテスト(End-to-End テスト): ユーザーが実際にブラウザで操作するのと同じ動作を自動で再現し、アプリケーション全体が正しく動くことを確認するテストです。「ログインボタンを押したら、ダッシュボードに遷移する」といった、画面をまたいだ一連の操作を検証します
- CI(継続的インテグレーション): コードの変更をリポジトリにプッシュするたびに、自動でビルドやテストを実行する仕組みです。本の校正作業に例えるなら、「原稿を修正するたびに自動で誤字脱字チェックが走る」イメージです
- Node.js と npm(または yarn/pnpm)の基本的な使い方
なぜPlaywrightを選ぶのか
E2Eテストツールには Selenium や Cypress などの選択肢がありますが、Playwrightには以下の強みがあります。
| 観点 | Playwright | Cypress | Selenium |
|---|---|---|---|
| 対応ブラウザ | Chromium, Firefox, WebKit | Chromiumベースのみ | 全ブラウザ |
| 並列実行 | 標準で対応 | 有償プランで対応 | 別途Grid構築が必要 |
| 自動待機 | 組み込み済み | 組み込み済み | 手動実装が必要 |
| 言語 | TS/JS, Python, Java, C# | JS/TSのみ | 多言語対応 |
| Trace Viewer | 標準搭載 | Dashboard(有償) | なし |
PlaywrightはMicrosoftが開発・メンテナンスしており、モダンWebアプリとの相性が良く、セットアップも簡単です。特に「自動待機(Auto-waiting)」機能は、要素が表示されるまで自動で待ってくれるため、テストの安定性が格段に高まります。
ハンズオン:セットアップから最初のテストまで
Step 1: プロジェクト初期化
# プロジェクトディレクトリを作成
mkdir my-e2e-tests && cd my-e2e-tests
# Playwrightをインストール(ブラウザも自動ダウンロード)
npm init playwright@latest
対話形式でいくつか質問されます。デフォルトのまま進めて問題ありません。完了すると以下の構造が作られます。
my-e2e-tests/
├── playwright.config.ts # 設定ファイル
├── tests/
│ └── example.spec.ts # サンプルテスト
├── package.json
└── node_modules/
Step 2: 設定ファイルを理解する
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// テストを並列実行する
fullyParallel: true,
// CI環境ではリトライしない
retries: process.env.CI ? 2 : 0,
// テスト失敗時にトレースを保存(デバッグ用)
use: {
trace: 'retain-on-failure',
// テスト対象のベースURL
baseURL: 'http://localhost:3000',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
ポイントは trace: 'retain-on-failure' です。テストが失敗したとき、その時点の画面状態やネットワークリクエストをタイムラインで再現できるトレースファイルが自動保存されます。
Step 3: 最初のテストを書く
例として、TODOアプリのログインフローをテストしてみます。
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test('ログインしてダッシュボードに遷移する', async ({ page }) => {
// ログインページへ移動
await page.goto('/login');
// メールアドレスとパスワードを入力
await page.getByLabel('メールアドレス').fill('user@example.com');
await page.getByLabel('パスワード').fill('password123');
// ログインボタンをクリック
await page.getByRole('button', { name: 'ログイン' }).click();
// ダッシュボードに遷移したことを確認
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'ダッシュボード' })).toBeVisible();
});
getByRole や getByLabel といったロールベースのロケーターを使うのがPlaywrightの推奨です。CSSセレクターやXPathに比べて、UIの見た目が変わってもテストが壊れにくいという利点があります。
Step 4: テスト実行
# 全テストを実行
npx playwright test
# UIモード(ブラウザの動きをリアルタイムで確認)
npx playwright test --ui
# 特定のファイルだけ実行
npx playwright test tests/login.spec.ts
# HTMLレポートを表示
npx playwright show-report
Page Object Modelで保守性を高める
テストが10個、20個と増えると、ページ構造の変更がすべてのテストに波及して修正地獄に陥ります。これを防ぐのがPage Object Model(POM)パターンです。
POMは「ページの操作方法をクラスにまとめ、テストからはそのクラスを使う」という設計パターンです。身近な例で言えば、リモコンのようなものです。テレビの内部構造が変わっても、リモコンの操作方法(ボタンの押し方)は変わりません。
実装例
// pages/login-page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
private readonly emailInput: Locator;
private readonly passwordInput: Locator;
private readonly loginButton: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('メールアドレス');
this.passwordInput = page.getByLabel('パスワード');
this.loginButton = page.getByRole('button', { name: 'ログイン' });
}
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();
}
}
// tests/login.spec.ts(POM版)
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
test('ログインしてダッシュボードに遷移する', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
ログインフォームのHTML構造が変わっても、修正するのは LoginPage クラスだけです。テストコード自体は一切変更不要になります。
プロジェクト構成の推奨
tests/
├── pages/ # Page Objectクラス
│ ├── login-page.ts
│ ├── dashboard-page.ts
│ └── cart-page.ts
├── fixtures/ # テストデータ・共通設定
│ └── test-data.ts
└── specs/ # テストファイル
├── login.spec.ts
├── search.spec.ts
└── purchase.spec.ts
GitHub ActionsでCIに組み込む
テストを書いただけでは、「実行を忘れる」リスクがあります。CIに組み込めば、プッシュやPR作成のたびに自動でテストが走るようになります。
ワークフロー定義
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload test report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 14
if: ${{ !cancelled() }} がポイントです。テストが失敗してもレポートは必ずアップロードされるので、失敗原因をHTMLレポートで確認できます。
よくある落とし穴と対処法
1. フレーキーテスト(不安定なテスト)
テストが「たまに落ちる」状態は、チームのCI信頼度を大きく損ないます。主な原因と対処法は以下のとおりです。
| 原因 | 症状 | 対処法 |
|---|---|---|
| タイミング依存 | 要素が表示される前にクリック |
getByRole 等のロケーターを使う(自動待機が効く) |
| テスト間の依存 | 実行順序で結果が変わる | 各テストを独立させ、前提データをテスト内で準備する |
| アニメーション | 要素の位置がずれてクリックが空振り | CSSアニメーションをテスト時に無効化する |
| 外部API依存 | APIが遅い・不安定 |
page.route() でモックする |
2. テストが遅い
E2Eテストはユニットテストに比べて実行時間が長くなりがちです。以下の戦略で対処します。
-
並列実行:
fullyParallel: trueを有効にする(デフォルトで有効) - テストの絞り込み: CIではクリティカルパスのみ実行し、全テストは夜間バッチに回す
- シャーディング: GitHub Actionsのmatrix戦略で複数マシンに分散実行する
3. 手動テストと自動テストの使い分け
すべてを自動化する必要はありません。テストピラミッドの考え方を意識しましょう。
| 層 | 速度 | コスト | 検証範囲 |
|---|---|---|---|
| ユニットテスト | 高速(数秒) | 低い | 関数・メソッド単体 |
| 統合テスト | 中速(数十秒) | 中程度 | API・DB連携 |
| E2Eテスト | 低速(数分) | 高い | ユーザー操作全体 |
E2Eテストは「ユーザーにとって致命的なフロー」(ログイン、決済、主要な画面遷移)に絞り、ロジックの検証はユニットテストで担保するのがベストプラクティスです。
4. デバッグのコツ
テストが失敗したら、以下の順に調査します。
-
HTMLレポート:
npx playwright show-reportでエラー箇所とスクリーンショットを確認 -
Trace Viewer:
npx playwright show-trace trace.zipで操作のタイムラインを再現 -
UIモード:
npx playwright test --uiでステップ実行しながらブラウザの状態を目視確認 -
デバッグモード:
PWDEBUG=1 npx playwright testでブレークポイントを使ったデバッグ
特にTrace Viewerは強力で、失敗時点の画面状態・ネットワークリクエスト・コンソールログをすべてタイムラインで確認できます。
まとめ
手動テストから自動テストへの移行は、一気にやる必要はありません。まずは最も重要な1つのフロー(ログインや主要な業務フロー)をPlaywrightで自動化し、GitHub Actionsに組み込むところから始めてみてください。
始め方のステップ:
-
npm init playwright@latestでセットアップ - 最も重要なユーザーフローを1本テストとして書く
- GitHub Actionsのワークフローを追加してCIに組み込む
- テストが増えてきたらPage Object Modelで整理する
手動テストと自動テストは排他的ではありません。探索的テストやUXの確認は引き続き手動で行い、繰り返し実行する回帰テストをPlaywrightに任せる——この役割分担が、チームの品質と生産性を両立させる鍵です。