同じアプリに、2つのAIで E2E テストをかけてみた
前回の記事で、Claude Cowork(デスクトップ操作AI)に31画面のE2Eテストを丸投げした話を書いた。
あの記事の続きで以下を試してみた。
- リグレッションテスト化 — PR ごとに Cowork でテストを自動実行し、UI 崩れを CI レベルで検出
- ビジュアルリグレッション — スクショの差分比較を自動化
Claude Code CLI に Playwright のE2Eテストコードを書かせた。
同じアプリ、同じ31画面。片方は「AIがブラウザを目で見て確認する」、もう片方は「AIが自動テストコードを書いてプログラムで検証する」。
両方やったから、比較できる。 どっちが強いのか。どう使い分けるのか。実データで語る。
先に結論
どっちが優れているか、ではない。テストの「層」が違う。
| Cowork(目視AI) | Claude Code + Playwright | |
|---|---|---|
| 一言で言うと | AIがブラウザを操作して目で見る | AIがテストコードを書いて自動実行 |
| 得意なこと | UIの見た目崩れ・レイアウト不整合 | ナビゲーション・認証・要素の存在 |
| 再現性 | 毎回微妙に違う(AIの判断に依存) | 完全に同じ(コードだから) |
| CI統合 | 不可能 | 可能(GitHub Actions等) |
| バグ発見数 | 7件報告(うち3件が本物) | 10件のテスト失敗(うち4件が本物のバグ) |
| 実行時間 | 約40分 | 約30秒 |
| 人間の介入 | ログインだけ手動 | ゼロ(完全自動) |
両方やって見えた結論:Cowork は「探索テスト」、Playwright は「回帰テスト」。 フェーズが違う。
比較対象のアプリ(前回と同じ)
| 指標 | 数値 |
|---|---|
| テスト対象画面数 | 31画面 |
| TypeScript 総行数 | 約 65,000 行 |
| API エンドポイント数 | 25 本 |
| 技術スタック | Next.js 16.1 + shadcn/ui + Hono + Drizzle ORM |
| 認証 | Google OAuth → JWT (HS256) |
第1章:Playwright E2EテストをClaude Codeに書かせる
人間が書いたコード:0行
まず衝撃的な事実から。
Playwright のE2Eテストコード、1行も自分で書いていない。
Claude Code CLI に「E2Eテストを実装して」と依頼した。CLI がコードベースを読み、認証フローを理解し、テストコードを9ファイル生成した。
e2e/
├── playwright.config.ts # PC(1280px) + Mobile(390px) 2プロジェクト
├── global-setup.ts # 認証Cookie注入
├── .env.e2e # テスト用環境変数
├── helpers/
│ ├── jwt.ts # JWT生成ユーティリティ
│ └── test-fixtures.ts # ★ 認証バイパスの核心
└── specs/
├── 01-smoke.spec.ts # 認証・リダイレクト確認
├── 02-navigation.spec.ts # サイドバー全13リンク遷移
├── 03-dashboard.spec.ts # ダッシュボード表示
├── 04-masters.spec.ts # マスタ5種の一覧・登録
├── 05-projects.spec.ts # 案件管理
├── 06-pricings.spec.ts # 単価一覧
├── 07-evidence.spec.ts # エビデンスメール
├── 08-approvals.spec.ts # 承認フロー
└── 09-admin.spec.ts # 管理者・請求
計 42テストケース(PC) + 42テストケース(モバイル) = 84テスト。
一番難しかったところ:認証バイパス
E2Eテストの最大の壁は認証。このアプリは Google OAuth → JWT という認証フローで、テストから Google ログインは実行できない。
Claude Code は既存のバックエンドコード(jwt.ts, proxy.ts)を読んで、3層の認証バイパスを自動設計した。
ここが面白い。 最初の実装では Layer 2 が無かった。refresh_token を JWT として Cookie に入れる設計だった。しかし実際に動かすと 401 エラー。
なぜか? refresh_token は本番では opaque トークン(DB にハッシュ保存して検証するランダム文字列)であって、JWT ではなかったから。
// test-fixtures.ts — Claude Code が書いた認証バイパスの核心部分
// 背景: refresh_token は本番では opaque トークン(DB ハッシュ検証)のため、
// JWT を Cookie に入れても /auth/refresh は 401 になる。
// route mock で /auth/refresh をバイパスし、有効な JWT access token を返す。
export const test = base.extend({
page: async ({ page }, use) => {
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) throw new Error('JWT_SECRET is required');
const accessToken = await generateE2EToken(jwtSecret, 'admin');
// /auth/me → ユーザー情報を返す
await page.route('**/auth/me', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
user: {
id: 'e2e-user-id', email: 'e2e@test.com',
displayName: 'E2E User', role: 'admin', activeRole: 'admin',
},
}),
});
});
// /auth/refresh → 有効な access token を返す
await page.route('**/auth/refresh', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ accessToken }),
});
});
await use(page);
},
});
AI がバックエンドの認証実装を読んで、テスト用のバイパス戦略を自動設計した。 これは人間でも迷うポイント。Cookie 注入 + Route Mock の2段構えという解法を、コードリーディングだけで導き出している。
第2章:最初のテスト実行——10個の失敗と戦った記録
Claude Code が生成したテストコード、最初から全部通ったわけではない。
最初の実行結果:
Running 84 tests using 6 workers
✓ 72 passed
✗ 10 failed ← ここから戦いが始まる
- 2 skipped
10個の失敗。 でも、これが面白かった。失敗の原因が「テストコードのバグ」と「アプリの実際の問題」の混在だったから。
失敗パターン①:Playwright の strict mode 違反(6件)
Playwright には strict mode がある。ロケーターが複数要素にマッチすると即エラー。
Error: locator('nav') resolved to 2 elements
<nav> が2つあった。 メインナビゲーションとパンくずリスト。Claude Code の最初の実装は page.locator('nav') と書いていたが、これだと曖昧。
修正:
// Before: 曖昧なロケーター
await expect(page.locator('nav')).toBeVisible();
// After: aria-label で特定
await expect(
page.getByRole('navigation', { name: 'メインナビゲーション' })
).toBeVisible();
同様の問題がモバイルのメニューボタンでも:
// Before: 正規表現が広すぎて2つのボタンにマッチ
const menuButton = page.getByRole('button', { name: /メニュー|menu/i });
// → "メニューを開く" と "ユーザーメニュー" の両方にマッチ!
// After: 正確な名前を指定
const menuButton = page.getByRole('button', { name: 'メニューを開く' });
これは「AIの推測」vs「実際のDOM」のギャップ。 Claude Code はコードを読んでロケーターを推測するが、実行時のDOMは予想と違うことがある。shadcn/ui のコンポーネントが内部で複数の要素を生成するケースなど。
失敗パターン②:存在しないUIを期待(2件)
// Claude Code が書いたコード
await expect(page.locator('[data-testid="stat-card"]')).toBeVisible();
ダッシュボードに「統計カード」があると推測したが、実際にはタブUI だった。
// 修正: 実際のUIに合わせる
await expect(
page.getByRole('tab', { name: '担当案件' })
).toBeVisible();
AIはコードから「ダッシュボードには統計情報がある」と推論したが、UIの実装形式(カード vs タブ)までは正確に予測できなかった。
失敗パターン③:API 401 でデータが表示されない(2件)
エビデンスメールと管理者ページで、テーブルの存在確認が失敗。
原因:E2E テストの JWT_SECRET とバックエンドの JWT_SECRET が違う。Route Mock で /auth/me と /auth/refresh はバイパスしているが、その他のAPIエンドポイント(/api/evidence-emails 等)は実際のバックエンドに到達する。 バックエンドは別の JWT_SECRET で検証するので 401。
// Before: データ依存のテスト(API 401 で失敗)
await expect(page.locator('table')).toBeVisible();
// After: UI構造のテスト(API不要)
await expect(
page.getByRole('heading', { name: /エビデンスメール管理/ })
).toBeVisible();
教訓:E2Eテストはデータの存在に依存させない。UI構造の存在を確認する。
失敗パターン④:dotenv がワーカーに伝播しない(1件)
これが一番ハマったやつ。
Error: JWT_SECRET is required
playwright.config.ts で dotenv.config() を呼んでいるのに、テストワーカーで process.env.JWT_SECRET が undefined。
原因:Playwright はテストをワーカープロセスで実行する。 playwright.config.ts はメインプロセスで実行されるが、その環境変数はワーカーに引き継がれない。
// test-fixtures.ts の冒頭に追加して解決
import dotenv from 'dotenv';
import path from 'node:path';
// 各ワーカーが自分で .env.e2e を読み込む
dotenv.config({ path: path.resolve(__dirname, '../.env.e2e') });
これは Playwright 公式ドキュメントにも明記されていない落とし穴。 Claude Code も最初は引っかかったが、エラーメッセージから原因を推測して自己修正した。
第3章:全テスト通過
10個の失敗を1つずつ修正し、最終結果:
===== PC (Desktop Chrome 1280x720) =====
42 passed (29.7s)
===== Mobile (Pixel 5 390x844) =====
41 passed, 1 skipped (29.1s)
Total: 83 passed, 1 skipped
Time: 約30秒
84テスト中 83パス、1スキップ(モバイルでサイドバー非表示は仕様)。約30秒。
第4章:Cowork vs Playwright——同じアプリで何が違ったか
さて、ここからが本題。同じアプリに対して2つのアプローチを適用した結果の比較。
バグ発見力の比較
| バグ | Cowork | Playwright |
|---|---|---|
| サイドバーのアバターがメインコンテンツに重なる(スマホ) | 発見 | 未検出 |
| カレンダーのアジェンダが下に落ちる(PC) | 発見 | 未検出 |
| パンくずが英語表記 | 発見 | 未検出 |
<nav> が2つある(strict mode違反) |
未検出 | 発見 |
| モバイルメニューボタンのaria-label不整合 | 未検出 | 発見 |
| フィルタUIの実装がinput/selectではない | 未検出 | 発見 |
| API 401 でテーブル未表示 | 報告なし | 発見 |
| ダッシュボードのstat-card不在 | 未検出 | 発見 |
気づいた? 発見するバグの種類が完全に違う。
- Cowork が見つけるバグ: レイアウト崩れ、要素の重なり、テキストの言語不整合——「見た目」の問題
- Playwright が見つけるバグ: DOM構造の不整合、aria属性の欠落、APIレスポンスエラー——「構造」の問題
なぜ検出範囲が違うのか
Cowork はスクリーンショットを撮って「目で見ている」。だから ピクセルレベルのズレ を検出できる。一方で DOM の属性や API のステータスコードは見えない。
Playwright は DOM を操作して「プログラムで検証している」。だから 要素の存在や属性の値 を正確にチェックできる。一方でレイアウトの「見た目」は判断できない(toHaveScreenshot() を使えば別だが、ベースライン画像が必要)。
第5章:6つの軸で徹底比較
1. セットアップコスト
| Cowork | Playwright | |
|---|---|---|
| 初回準備 | CLI でプロンプト生成(30分) | CLI でテストコード生成 + デバッグ(2時間) |
| 認証対応 | 人間が手動ログイン(10秒) | JWT生成 + Cookie注入 + Route Mock 実装 |
| 2回目以降 | プロンプト渡すだけ |
pnpm test:e2e 実行するだけ |
初回は Cowork の圧勝。 プログラミング不要。日本語でチェックリストを書くだけ。
Playwright は認証バイパスの実装だけで相当のコード量がある(global-setup.ts + test-fixtures.ts + jwt.ts で約100行)。しかもデバッグが必要。
2回目以降は Playwright の圧勝。 コマンド1つで30秒。Cowork は毎回40分かかる。
2. 実行速度
| Cowork | Playwright | |
|---|---|---|
| 実行時間 | 約40分 | 約30秒 |
| 並列度 | 1画面ずつ順次 | 6ワーカー並列 |
| 再実行コスト | 同じ40分 | 同じ30秒 |
| PR ごとの実行 | 現実的でない | CI に組み込める |
80倍の速度差。 Playwright は6ワーカーで並列実行するため、84テストが30秒で終わる。
3. 再現性
| Cowork | Playwright | |
|---|---|---|
| 同じテストで同じ結果? | No — AIの判断にブレがある | Yes — コードだから決定的 |
| False Positive(偽陽性) | 7件中4件(57%) | 10件中6件(60%) |
| False Negative(見落とし) | 不明(目視だから) | 0件(コードが通れば確実) |
興味深いのは、偽陽性率はほぼ同じ ということ。Cowork は「仕様通りの挙動」をバグと報告し、Playwright は「テストコードの前提」と「実際のUI」のギャップで失敗する。
ただし 再現性は Playwright が圧倒的に上。 Cowork は同じプロンプトで2回テストしても、スクショのタイミングや判断基準が微妙に変わる。Playwright はコードなので、同じ入力なら必ず同じ結果。
4. CI/CD 統合
| Cowork | Playwright | |
|---|---|---|
| GitHub Actions | 不可 | 可能 |
| PR ブロック | 不可 | 可能(テスト失敗で merge 禁止) |
| 差分テスト | 不可 | 可能(変更に関連するテストだけ実行) |
CI に組み込めるかどうか、これが最大の違い。
Cowork はデスクトップアプリなので CI パイプラインには載らない。Playwright は npx playwright test をそのまま GitHub Actions で実行できる。
# GitHub Actions での Playwright 実行(イメージ)
- name: E2E Tests
run: pnpm test:e2e
env:
JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
CI: true
PR を出したら自動で84テストが走り、失敗したらマージできない。 これは Cowork にはできないこと。
5. メンテナンスコスト
| Cowork | Playwright | |
|---|---|---|
| UI変更時 | プロンプトのテキストを修正 | テストコードを修正 |
| 新画面追加時 | チェック項目を追記 | spec ファイルを追加 |
| 壊れやすさ | 低い(自然言語は柔軟) | 高い(DOM変更で即死) |
ここは Cowork の強み。 自然言語のプロンプトは UI の細かい変更に強い。ボタンのテキストが「新規作成」から「新規登録」に変わっても、Cowork は文脈で判断できる。
Playwright は getByRole('button', { name: '新規作成' }) が即失敗する。ロケーターの修正が必要。
実際、今回の開発でも shadcn/ui のコンポーネントが内部で複数要素を生成する問題 で何度もロケーター修正が必要だった。
6. バグの深刻度
| Cowork が見つけたバグ | Playwright が見つけたバグ | |
|---|---|---|
| ユーザー影響 | 高い(見た目の崩れは即座にわかる) | 中程度(構造問題は気づきにくい) |
| セキュリティ | 低い | 高い(認証バイパス・401エラー検出) |
| アクセシビリティ | 低い | 高い(aria-label欠落の検出) |
Cowork が見つける「アバターの重なり」は、ユーザーが即座に気づくバグ。 一方 Playwright が見つける「aria-labelの不整合」は、スクリーンリーダーユーザー以外は気づかない。
しかしアクセシビリティや認証の問題は、見つかったときのインパクトが大きい。
第6章:じゃあどう使い分けるのか
テストピラミッドで考える
Playwright と Cowork は ピラミッドの同じ層にいるように見えて、実は違う層をカバーしている。
- Playwright → E2E テスト層。「ページ遷移が正しいか」「認証が機能するか」「UIの構造が正しいか」
- Cowork → ビジュアルリグレッション層。「見た目が崩れていないか」「レイアウトが意図通りか」
実践的な使い分けフロー
| タイミング | 使うツール | 目的 |
|---|---|---|
| PR ごと | Playwright | 回帰テスト(壊れていないか確認) |
| スプリント末 | Cowork | 探索テスト(新しいバグがないか発見) |
| UI大幅変更後 | Cowork | レイアウト崩れの一斉チェック |
| リリース前 | 両方 | 最終確認 |
一言で言うなら
Playwright は「門番」、Cowork は「探偵」。
Playwright は毎回同じチェックを高速に繰り返す門番。通れなければ即ブロック。
Cowork は新しい問題を目で見て発見する探偵。ただし毎回雇うにはコストが高い。
第7章:Claude Code が Playwright コードを書く過程で起きた面白いこと
AI が AI のコードをデバッグする
Claude Code が書いた最初のテストコードは10件失敗した。そして Claude Code 自身がエラーメッセージを読んで修正した。
Human: playwrightで検証してほしい
Claude: (テスト実行) → 10 failed
Claude: strict mode違反ですね。locator('nav') が2要素にマッチしています。
getByRole('navigation', { name: 'メインナビゲーション' }) に修正します。
Claude: (再実行) → 7 failed
Claude: モバイルのメニューボタンも同じ問題です。正規表現が広すぎました。
'メニューを開く' に限定します。
Claude: (再実行) → 4 failed
...
Claude: (最終実行) → 83 passed, 1 skipped ✓
AI が書いたコードを、AI がデバッグして、AI が修正する。 人間は「検証してほしい」と1回言っただけ。
これは Cowork にはできない。Cowork はバグを「報告」するだけで、コードの修正はしない。Claude Code は 報告 → 原因特定 → 修正 → 再検証 のサイクルを自律的に回す。
dotenv 問題——AIも引っかかるPlaywrightの罠
一番印象的だったのは、dotenv のワーカー伝播問題。
playwright.config.ts で dotenv.config() を呼んでいるのに、テストワーカーで環境変数が undefined。これは Playwright のプロセスモデル を理解していないと解決できない。
Claude Code はエラーメッセージ JWT_SECRET is required を見て、「config では読めているのにワーカーで読めない → ワーカーは別プロセス → ワーカーでも dotenv を読む必要がある」と推論した。
// test-fixtures.ts(ワーカープロセスで実行される)
import dotenv from 'dotenv';
import path from 'node:path';
// playwright.config.ts の dotenv.config() は引き継がれない。
// 各ワーカーが自分で .env.e2e を読み込む必要がある。
dotenv.config({ path: path.resolve(__dirname, '../.env.e2e') });
この問題、人間の開発者もよくハマるやつ。 AI でもハマったが、AI は自力で解決した。
第8章:コスト比較(現実的な話)
Cowork のコスト
| 項目 | コスト |
|---|---|
| プロンプト生成(CLI) | Claude Code の API 使用量(数ドル) |
| テスト実行(Cowork) | Cowork のクレジット(40分分) |
| トリアージ(CLI) | Claude Code の API 使用量(数ドル) |
| 合計(1回) | 推定 $5〜10 |
Playwright のコスト
| 項目 | コスト |
|---|---|
| テストコード生成(CLI) | Claude Code の API 使用量(十数ドル)※初回のみ |
| デバッグ・修正(CLI) | Claude Code の API 使用量(十数ドル)※初回のみ |
| テスト実行(ローカル) | $0(ローカル実行) |
| テスト実行(CI) | GitHub Actions の実行時間(微量) |
| 合計(初回) | 推定 $20〜30 |
| 合計(2回目以降) | ほぼ $0 |
初回コストは Playwright が高い。 テストコードの生成とデバッグに Claude Code を使い込むから。
2回目以降のランニングコストは Playwright がゼロに近い。 コード実行にAIは不要。
一方 Cowork は毎回クレジットを消費する。10回実行すれば $50〜100。
損益分岐点は約3回。 3回以上テストを実行するなら Playwright の方が安い。
第9章:それぞれの「こういうプロジェクトに向いている」
Cowork が向いているケース
- プロトタイプ / MVP — テストコードを書くほど安定していない。週単位で UI が変わる
- デザイン重視のアプリ — ピクセルレベルの崩れが致命的(LP、ECサイト、ポートフォリオ)
- 1回限りのテスト — リリース前の最終確認。CI に載せるほどではない
- 非エンジニアがテスト — プロンプトを日本語で書くだけ。プログラミング不要
Playwright が向いているケース
- 長期運用のアプリ — 機能追加のたびにリグレッションテストが必要
- チーム開発 — PR ごとに自動テストが走る安心感
- 認証 / 権限が複雑 — Route Mock で任意のロール・状態をテスト可能
- CI/CD パイプライン — テスト通過をデプロイの前提条件にしたい
両方使うべきケース(今回のパターン)
- 業務アプリ — 機能は安定しているが画面数が多い。Playwright で回帰テスト + Cowork で定期的な目視確認
- PC / モバイル両対応 — Playwright で機能テスト、Cowork でレスポンシブの見た目確認
- アクセシビリティ対応 — Playwright で aria 属性チェック、Cowork でスクリーンリーダー想定の操作確認
まとめ:使い分け早見表
| 判断軸 | Cowork を使う | Playwright を使う |
|---|---|---|
| テスト頻度 | 月1回以下 | PR ごと / 日次 |
| UIの安定度 | 頻繁に変わる | 安定している |
| 重視する品質 | 見た目の美しさ | 機能の正確性 |
| チーム規模 | 1人 / 小規模 | 複数人 / CI必須 |
| 技術スキル | プログラミング不要 | Playwright の知識必要 |
| コスト感覚 | 単発なら安い | 長期運用なら安い |
| 発見するバグ | レイアウト崩れ | 構造・認証・ナビゲーション |
そして最強は「両方使う」こと。
Playwright を CI に組み込んで PR ごとの回帰テストを自動化。Cowork をスプリント末に実行して目視の探索テスト。
門番と探偵の両方を雇え。
再現手順
Cowork でやる場合(前回記事参照)
1. Claude Code CLI でテストプロンプトを生成
2. 人間が手動ログイン
3. Cowork にファイルパスを渡す(1行)
4. 40分待つ
5. レポートをCLIでトリアージ
Playwright でやる場合(今回の記事)
1. Claude Code CLI に「E2Eテストを実装して」と依頼
2. CLIがテストコードを生成(約10ファイル)
3. pnpm test:e2e で実行(30秒)
4. 失敗があればCLIが自動修正
5. CIに組み込んで自動化
両方やる場合(おすすめ)
1. Playwright を先に構築(CI組み込み)
2. PRごとにPlaywrightが自動実行
3. リリース前にCoworkで目視確認
4. Coworkが見つけたバグ → Playwright のテストケースに追加
→ 次回以降は自動で検出される
Cowork が見つけたバグを Playwright のテストケースに変換する。 これが最強のサイクル。探偵が見つけた問題を、門番のチェックリストに追加する。
おわりに
正直、「AI にテストを任せる」と一口に言っても、方法が2つあるとは思っていなかった。
Cowork に丸投げしたときは「面倒な目視確認を代行してもらった」という感覚だった。Playwright を書かせたときは「テストの自動化基盤を構築した」という感覚。
やっていることは全然違う。 でもどちらも「AIがE2Eテストをする」と表現できてしまう。
大事なのは、何を検証したいのか を先に決めること。
- 「見た目が崩れていないか確認したい」→ Cowork
- 「機能が壊れていないことを毎回保証したい」→ Playwright
- 「両方やりたい」→ 両方やれ。AIなら両方のコストが人間の1/10になる
31画面 × PC/モバイル = 62パターン。
Cowork なら40分。Playwright なら30秒。手動なら半日。
そして 両方から異なるバグが見つかる。 どちらか一方では、片方のバグを見逃す。
テストに「これだけやれば十分」はない。でも AIを使えば、「もう一層追加する」コストが劇的に下がる。その結果、品質が変わる。
門番と探偵、両方雇おう。