playwright-testing について
playwright-testing は、信頼性の高い Playwright のエンドツーエンドテストを書く開発者向けのスキルです。境界条件やエラーの網羅、固定時間ではなく条件に基づく待機、テスト目的に応じたモックを重視します。
主な機能
- 01正常系・エラー・境界条件の網羅
- 02固定時間ではなく条件に基づく待機
- 03テスト目的に応じた API モック
- 04page.locator() による要素検索
- 05ネットワークの傍受とリクエスト契約の検証
活用シーン
- 01リリース前のエンドツーエンドテスト作成
- 02CI での断続的な失敗の削減
- 03エラー処理とリトライの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zenobi-us/ dotfiles --skill playwright-testingインストール後、Claude Code で/playwright-testingを入力して読み込まれたか確認します。
詳細
Playwright によるテスト
概要
Playwright のテストが失敗する原因は、予測可能な 3 つのパターンに分かれます。カバレッジ不足(エッジケースを確認せずにリリースする)、脆弱な待機(固定タイムアウトによる不安定なテスト)、不明確なモック方針(すべてモックするか、すべてステージング環境でテストするかのどちらかに偏る)です。このスキルでは、時間に追われている状況でも信頼性の高いテストを書くためのパターンを紹介します。
基本原則:リリース前にカバレッジを網羅します。待機は常に決定的な方法で行います。テストの目的に応じて戦略的にモックします。
使用する場面
このスキルが必要な兆候:
- デプロイ期限が迫る中で E2E テストを書いている
- 固定値を使って
waitForSelector()やwaitForTimeout()を呼び出している - API をモックするべきか、ステージング環境でテストするべきか判断できない
- ローカルでは成功するのに、CI では不安定になるテストがある
- エッジケースのカバレッジを「次のスプリント」に先送りしている
重要なパターン:リリース前にカバレッジを網羅する
プレッシャーのかかる状況: テストは動いています。期限まで 30 分です。エッジケースの確認には 15 分かかります。今すぐリリースしますか?
答え: いいえ。次の条件を満たした場合にのみリリースします。
- ハッピーパステストが成功する ✓
- エラーケースをテストしている(ネットワーク障害、タイムアウト、データの欠落)
- リトライロジックを検証している
- 境界条件に対応している
順序が重要な理由: テストカバレッジが不完全だと、見つかりにくいバグが生じます。エッジケースを先送りすると、テストされていない失敗経路をデプロイすることになります。最初にそこに遭遇するのはユーザーです。
例外はありません:
- 「次のスプリントで追加する」(実際には追加しません)
- 「エッジケースは起こりにくい」(本番環境では起こります)
- 「ハッピーパステストだけで十分」(十分ではありません)
待機戦略:時間ベースではなく条件ベース
問題点: デフォルトが 5 秒の page.waitForSelector() のような固定タイムアウトには、次の問題があります。
- 高速なシステムでは遅くなる(不要な待機が発生する)
- 低速なシステムでは不安定になる(タイムアウトが短すぎる)
- 実際の問題を隠してしまう(何を待っていたのでしょうか?)
- リファクタリングを先送りする言い訳になる(「後で直す」)
パターン:
// ❌ BAD: Fixed timeout, hides what you're waiting for
await page.waitForSelector('.loading', { timeout: 5000 });
await page.click('button');
// ✅ GOOD: Explicit condition, timeout is safety net
// Wait for loading spinner to appear, proving async work started
await page.locator('.loading').waitFor({ state: 'visible' });
// Wait for it to disappear, proving async work completed
await page.locator('.loading').waitFor({ state: 'hidden' });
await page.click('button');
// ✅ GOOD: Custom condition when standard waits don't fit
async function waitForApiCall(page: Page, method: string) {
let apiCalled = false;
page.on('response', (response) => {
if (response.request().method() === method) {
apiCalled = true;
}
});
// Keep checking until API was called
await page.waitForFunction(() => apiCalled);
}
重要な理由: 条件ベースの待機を使うと、何をテストしているのかが明確になります。高速なシステムではより速く、低速なシステムではより確実に動作し、タイムアウトを待つのではなくタイミングの問題をすぐに検出できます。
次の対象に適用します:
- DOM の状態変化(表示/非表示/アタッチ済み)
- API 呼び出し(ネットワークのインターセプト)
- データの更新(テキストコンテンツの変化)
- フォームの準備状態(ボタンの有効化/無効化)
重要: 条件ベースの待機を「将来のリファクタリング」に先送りしないでください。固定タイムアウトを使ったテストは、次のようになります。
- ローカルでは成功する(マシンが高速)
- CI では不安定になる(負荷がかかっている)
- いつまでも不安定なままになる(後回しにしたリファクタリングは実行されない)
対応: 条件ベースの待機に必要なのは、わずか 2 行追加するだけです。今書いてください。「後で」でも、「ファイルを編集するときに」でも、「バグが発生したら」でもありません。今です。
タイムアウトは安全策としてのみ使用します:
// Reasonable defaults: 5s for navigation, 10s for complex async
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
await page.locator('[data-testid]').waitFor({ timeout: 10000 });
モック戦略:二者択一ではなく、目的に応じて選ぶ
問題点: どちらも失敗する 2 つの極端な方法があります。
- すべてモックする: テストには成功しますが、統合バグを検出できません(モックは実際の動作とは異なることがあります)
- すべてステージング環境でテストする: インフラの不安定さが原因でテストが不安定になります(ステージング環境は実環境に近いものの、信頼性に欠けます)
パターン:テストの目的に応じたハイブリッド方式
// INTENT: Test UI logic, not API integration
// → Mock the API, test DOM updates
test('displays user data when loaded', async ({ page }) => {
await page.route('/api/user', route => {
route.abort(); // Simulate network failure
});
await page.goto('/profile');
await expect(page.locator('.error-message')).toContainText('Failed to load');
});
// INTENT: Test API integration, not UI
// → Hit staging, verify contract is correct
test('payment endpoint returns correct schema', async ({ page }) => {
// Hit real staging, prove response matches what UI expects
const response = await page.request.post(
`${process.env.STAGING_API}/payment`,
{ data: { amount: 100 } }
);
expect(response.ok()).toBeTruthy();
const json = await response.json();
expect(json).toHaveProperty('transactionId');
expect(json).toHaveProperty('status');
});
// INTENT: Test complete critical flow
// → Hybrid: mock non-critical paths, hit staging for critical ones
test('checkout flow succeeds end-to-end', async ({ page }) => {
// Mock product catalog (doesn't change)
await page.route('/api/products', route => {
route.continue({ response: mockProducts });
});
// Hit real staging for payment (critical + mature)
// Hit real staging for order confirmation (critical + stable)
// Results in fast + reliable + safe tests
});
判断基準:
- UI ロジックテスト(テストの 99%)→ API をモックし、UI の応答をテストします
- コントラクトテスト(テストの 1~2%)→ 重要な統合箇所ではステージング環境に接続します
- ステージング環境が不安定な場合 → モックを増やし、代わりに UI の回復力をテストします
- カバレッジに不足がある場合 → ステージング環境では簡単に発生しないエラーケースのモックシナリオを追加します
よくある誤りとその正当化
| 正当化 | 実態 | 対応 |
|---|---|---|
| 「エッジケースのテストは次のスプリントに回そう」 | 回しません。エッジケースは常にテストされないままリリースされます。 | 網羅的なカバレッジを確保してリリースするか、リリースしないでください。例外はありません。 |
| 「固定タイムアウトで十分」 | ローカルでは動作しても、CI では不安定になります。条件ベースの待機は難しくありません。 | 条件ベースの待機を使ってください。「後で」ではなく、今すぐです。 |
| 「作業しながら条件ベースの待機にリファクタリングしよう」 | しません。固定タイムアウトはいつまでも残り、CI はいつまでも不安定なままです。 | 最初から条件ベースの待機を書いてください。「次回」ではなく、今回です。 |
| 「手動テストでエッジケースをカバーできる」 | できません。手動テストではリグレッションを防げません。 | エッジケースの自動テストは必須です。どちらも重要です。 |
| 「すべてをモックするのが現実的だ」 | 現実的であるとは、テストが機能することです。すべてをモックすると、統合上のバグが隠れてしまいます。 | 重要な経路はステージング環境に対してテストしてください。それ以外は意図を明確にしてモックしてください。 |
| 「今のセットアップは複雑すぎてリファクタリングできない」 | そんなことはありません。条件ベースの待機は2行です。複雑さは口実にすぎません。 | 最初から正しい待機処理を書いてください。都合ではなく、リリースに必要な動作に変更してください。 |
要注意の兆候 — 作業を止めてください
次のようなことを言っていたら、作業を止めて最初からやり直してください。
- 「タイムアウトが不安定になったら直そう」
- 「エッジケースはリリースして、後で強化すればいい」
- 「今はこれで十分」
- 「このセットアップは特別だ」
- 「現実的に対処しているだけだ」
これらはすべて、テストカバレッジが不完全で、脆弱な待機処理がそのままリリースされることを意味します。削除して、網羅的なカバレッジと条件ベースの待機を備えたものに書き直してください。
実装
次の用途には、page.$() より page.locator()(推奨)を使ってください。
- 組み込みの待機処理
- より分かりやすいエラーメッセージ
- コード上で意図を明確にする
// Find element and wait for it to be visible
await page.locator('[data-testid="submit"]').waitFor({ state: 'visible' });
// Find and click in one step (waits for visibility first)
await page.locator('[data-testid="submit"]').click();
意図が明確なルート API を使ってください。
// All error cases for this path
await page.route('/api/checkout/**', route => {
if (Math.random() > 0.8) route.abort(); // 20% failure rate
else route.continue();
});
ネットワークをインターセプトして、契約を検証してください。
const requests: any[] = [];
await page.on('request', (request) => {
if (request.url().includes('/api')) {
requests.push({
url: request.url(),
method: request.method(),
postData: request.postData(),
});
}
});
// After test actions
expect(requests).toContainEqual(
expect.objectContaining({ method: 'POST', url: expect.stringContaining('/payment') })
);
実際の効果
これらのパターンを zenobi.us の e2e スイートに適用した結果:
- 固定タイムアウトによる不安定率が8%から0.3%に低下しました(条件ベースの待機)
- エッジケースのカバレッジが42%から94%に向上しました(リリース前の網羅性)
- テストの保守作業が60%減少しました(意図が明確になり、原因不明の失敗が減少)
- モック戦略により、ステージング環境での統合に対する信頼性を高めながら、CI の実行時間が35%短縮されました