訂正(2026-10-12): 欠落していたCIの全shard実行、認証状態、Storybookの検証範囲、ロケータ方針を修正しました。以下はテスト設計例で、特定のNext.jsアプリやStorybook全体の動作確認済み実装ではありません。
はじめに
Vibe Codingでコードの生成速度が上がるほど、人間が担保すべきは「振る舞いの定義(仕様)」=テストコードにシフトします。ところが皮肉なことに、AIにテストも「ただ書かせる」だけでは、CSSセレクタに依存した壊れやすいテストが量産されてしまいます。
本記事では以下の3つの問いに答えます。
- E2Eテスト(Playwright) をAIと正しく協働して生成するにはどうすればよいか?
- Storybookを使った表示・操作・視覚差分の検証をどう分けるか?
- 「AIが生成したテスト」を保守しやすい設計にするにはどのルールが必要か?
1. 前提:テストピラミッドと「誰が何を書くか」の再定義
▲
E2Eテスト
(少数・高コスト)
────────────────────
Integrationテスト
(中程度)
──────────────────────────
Unit / Component テスト
(多数・低コスト)
AI駆動開発時代における役割分担の推奨は以下の通りです。
| レイヤー | 主担当 | AIの使い方 |
|---|---|---|
| E2Eテスト | 人間が仕様を定義 | Playwright MCPで実ブラウザを操作してコード生成 |
| コンポーネント | AIが主体 | 仕様・必要な状態を人が確認し、ストーリーの草案を生成 |
| ユニットテスト | AIが主体 | 関数の仕様をプロンプトで渡してVitest等で生成 |
重要なのは「AIがテストを書く」ではなく 「AIは実装の詳細を書き、人間は仕様を定義する」 という分担です。
2. 【E2Eテスト】Playwright × AIの正しい協働
2-1. 「ChatGPTにただ書かせる」はなぜ失敗するか
コンテキストなしでLLMにPlaywrightのテストを生成させると、存在しないセレクタや架空のAPIエンドポイントを堂々と出力します。ソース、仕様、API契約に加え、DOM状態・URL遷移・操作後の状態を確認します。実装を見たテストだけでは、実装と同じ誤りを正解としてしまう場合があります。これを提供する仕組みが「Playwright MCP」です。
2-2. Playwright MCP によるテスト生成ワークフロー
Playwright MCPサーバーはAIエージェントに実ブラウザの制御権を与え、インタラクション後のページスナップショットをコンテキストとしてLLMに渡します。
# パッケージ導入例。クライアントへのMCPサーバー登録は別に必要
npm install -D @playwright/mcp
インストールだけではMCP接続は完了しません。利用クライアントへサーバーを登録し、利用可能なツール・接続先・権限を確認します。採用版をlockfile等に固定し、個人の認証済みセッションではなく専用のテスト環境を使います。
Cursor の .cursor/rules/playwright-test-gen.mdc の設計例です。
---
description: Playwright test generation with MCP
alwaysApply: false
---
## Playwright テスト生成ルール
あなたはPlaywright × TypeScriptのテスト生成エキスパートです。
### 必須手順
1. テスト生成前に必ず `browser_navigate` でURLを開くこと
2. ページスナップショットを取得してから操作を実行すること
3. 仕様とソースも確認し、仮定・未確認のセレクタやAPIを明示する
4. 生成後は `npx playwright test` で動作確認すること
### セレクタ優先順位(後述のルールと同期)
1. `getByRole()` — ユーザーが認識するロール
2. `getByLabel()` / `getByPlaceholder()` — フォーム要素
3. `getByText()` — 静的テキスト
4. `getByTestId()` — チームが定義した明示的なテスト契約
5. CSS / XPath — 構造変更に弱くなりやすいため、必要性を説明する
### 生成するテストコードの構造
- 重複や責務が増えたときにPage Object Modelを検討する
- テストデータはfixtures経由で注入
- UIを隔離する試験はmockを使い、実APIとの結合試験を別に設ける
2-3. E2Eテスト生成の実践フロー
以下がAIとの協働でテストを生成する具体的な手順です。
Step 1: 仕様をプロンプトで渡す
以下のシナリオでPlaywrightテストを生成してください:
- /login にアクセスする
- メールアドレス「test@example.com」とパスワードを入力してログインする
- /dashboard にリダイレクトされることを確認する
- ヘッダーにユーザー名が表示されることを確認する
※ 実際にブラウザでアクセスして、ページのスナップショットをとってからコードを生成してください。
Step 2: AIが生成したテストをPOMパターンに整形する
ファイル名: tests/e2e/pages/LoginPage.ts
import { type Page, type Locator } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("メールアドレス");
this.passwordInput = page.getByLabel("パスワード");
this.submitButton = page.getByRole("button", { name: "ログイン" });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
ファイル名: tests/e2e/auth.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "./pages/LoginPage";
test.describe("認証フロー", () => {
test("正常ログイン → ダッシュボードへリダイレクト", async ({ page }) => {
const loginPage = new LoginPage(page);
await page.goto("/login");
const password = process.env.TEST_PASSWORD;
if (!password) throw new Error("TEST_PASSWORD is required");
await loginPage.login("test@example.com", password);
// Web-first assertionで待機込みの検証
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading", { name: /ダッシュボード/ })).toBeVisible();
});
test("無効なパスワード → エラーメッセージ表示", async ({ page }) => {
const loginPage = new LoginPage(page);
await page.goto("/login");
// 外部APIをモック(ネットワーク依存を排除)
await page.route("**/api/auth/login", (route) =>
route.fulfill({ status: 401, contentType: "application/json", body: JSON.stringify({ error: "Invalid credentials" }) })
);
await loginPage.login("test@example.com", "wrongpassword");
await expect(page.getByRole("alert")).toContainText("メールアドレスまたはパスワードが正しくありません");
});
});
2-4. Playwright 設定ファイル
ファイル名: playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true, // テストを並列実行
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? "html" : "list",
use: {
baseURL: "http://localhost:3000",
// CIではtraceをfailure時の1回目リトライで取得(全件はパフォーマンス重い)
trace: "on-first-retry",
screenshot: "only-on-failure",
},
webServer: {
// 本番ビルドに対してテストを実行(開発モードとの挙動差を防ぐ)
command: "npm run build && npm run start",
url: "http://localhost:3000",
reuseExistingServer: false,
},
// Login tests start without authenticated storageState.
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
});
ログインを確認するこの設定では、認証済みstorageStateを渡しません。認証後の画面を試す別projectには、実装済みのsetupでログインして状態を保存し、そのsetupへの依存を指定します。旧稿の未実装setupと未作成user.jsonを参照する構成は取り下げます。状態ファイルにはcookie等が含まれるので公開・commitしません。サーバーのデータを変更する並列試験では、アカウントやデータの衝突も避けます。
reuseExistingServer: falseは既存の開発サーバーをそのまま使うのを防ぎます。TEST_PASSWORD等の準備、対象アプリの起動・API契約は別途必要です。mockした401は実認証APIの結合動作を証明しません。
3. 【コンポーネントテスト】Storybook × AI によるVisual Regression
3-1. Storybookの役割
StorybookはコンポーネントをUI単体で開発・テストする環境で、状態ごとの表示や操作を確認する候補になります。AIが境界条件を網羅する保証はなく、人が状態一覧と期待結果を確認します。
3-2. AIによるストーリー全量生成のプロンプト
以下のコンポーネントについて、Storybook v8 (CSF3形式) のストーリーファイルを生成してください。
コンポーネントコード:[ここにコンポーネントをコピペ]
必須カバー項目:
1. Default(通常状態)
2. Loading(ローディング中)
3. Error(エラー状態 + エラーメッセージ)
4. Empty(データなし)
5. 文字列が長い場合のオーバーフロー
6. モバイル表示(viewport: 375px)
各ストーリーには `play` 関数でユーザーインタラクションのテストも含めること。
Storybook 8系向けの草案例:
利用版によりテスト用importが異なります。この例はError/Empty/Mobileまで実装した全量例ではありません。Button側がonClickを受け取り、ネイティブのdisabled属性へ正しく伝える前提です。
ファイル名: components/ui/Button/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within, expect, fn } from "@storybook/test";
import { Button } from "./Button";
const meta = {
title: "UI/Button",
component: Button,
parameters: { layout: "centered" },
tags: ["autodocs"],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { children: "送信する", variant: "primary" },
};
export const Loading: Story = {
args: { children: "送信中...", variant: "primary", isLoading: true },
};
export const Disabled: Story = {
args: { children: "送信する", variant: "primary", disabled: true, onClick: fn() },
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
await expect(button).toBeDisabled();
await expect(args.onClick).not.toHaveBeenCalled();
},
};
export const LongText: Story = {
args: {
children: "これは非常に長いボタンラベルで、オーバーフローが発生するかを確認するためのテストです",
variant: "primary",
},
};
3-3. ビルド、interaction、視覚差分は別の検証
build-storybookが通っても、play関数がブラウザで実行されて通ったとは限りません。採用するStorybook版のテストrunnerやVitest連携等でinteractionを実行し、正常・不正操作のassertを確認します。画像比較には基準画像と差分のレビューが必要です。
ChromaticのexitZeroOnChanges: trueは、視覚差分があっても終了コード0にする設定です。Warning表示への変更や、差分の承認を自動で行う設定ではありません。差分をマージの条件にするなら、視覚レビューのstatusを必須にする等の別ゲートを設定します。
旧稿のchromaui/action@latestをそのまま利用する例は取り下げます。導入時は採用Actionの提供状態・commit・入力仕様を確認し、秘密を持つjobで不信なPRコードを実行しない構成にします。forkでsecretが使えない場合も、secretを渡すために安全境界を越えません。サービスへの送信・料金・保存条件も確認します。
Storybook interaction tests、Chromatic設定
4. 【保守性設計】壊れにくいテストの3原則
4-1. セレクタ戦略:「ユーザーが見るもの」でテストする
role/label等は利用者が見る意味を確認するのに向きます。test idも明示的な契約として公式で案内されています。ここでの優先順やCSS禁止はチーム独自の方針にできても、公式の一律必須規約ではありません。
// ❌ AIが生成しがちな壊れやすいセレクタ
page.locator('.submit-btn.is-primary');
page.locator('#root > div > form > button:last-child');
// ✅ Playwright公式推奨:ユーザーが認識するロールで指定
page.getByRole('button', { name: '送信する' });
page.getByLabel('メールアドレス');
page.getByPlaceholder('example@email.com');
// ✅ 明示的なテスト契約として data-testid を使用
page.getByTestId('submit-button');
4-2. data-testid の運用ルール
data-testidは表示文言が変わっても対象を特定する契約にできます。roleや名前の正しさを試す場合は、それも別にassertします。以下の命名規則を Cursor Rules に追記します。
ファイル名: .cursor/rules/nextjs-core.mdc への追記:
## Testing
- Playwright セレクタ優先順位: `getByRole` > `getByLabel` > `getByText` > `getByTestId`
- `data-testid`は明示的なテスト契約として使い、role/nameの確認も必要に応じて行う
- `data-testid` の命名規則: `[コンポーネント名]-[要素の役割]`(kebab-case)
- 例: `user-profile-avatar`, `login-form-submit`, `cart-item-count`
- CSS/XPathを使う場合は、DOM変更への耐性と必要性を確認する
- テスト専用属性を本番コードに残すことを許容する(削除しない)
4-3. Page Object Model で DRY なテスト設計
ページのDOM構造が変わってもテストロジックを壊さないために、セレクタを1か所に集約します。
tests/
├── e2e/
│ ├── pages/ # Page Object Models
│ │ ├── LoginPage.ts
│ │ ├── DashboardPage.ts
│ │ └── BasePage.ts # 共通メソッド(navigate, waitForLoad等)
│ ├── fixtures/
│ │ └── auth.setup.ts # 認証状態のセットアップ
│ └── *.spec.ts # テストファイル(POPを使う)
└── storybook/
└── *.stories.tsx # Storybookストーリー
// tests/e2e/pages/BasePage.ts
import { type Page, expect } from "@playwright/test";
export class BasePage {
constructor(protected page: Page) {}
async expectToast(message: string) {
await expect(
this.page.getByRole("status").filter({ hasText: message })
).toBeVisible();
}
}
networkidleを準備完了の判定として一律に待つのではなく、必要なUIのWeb-first assertionを使います。BasePageも必要な重複があるときに導入し、テストのためだけの抽象化を増やしません。
4-4. ソフトアサーションで複数項目を一括チェック
AIが生成したテストはアサーションが1つだけのことが多いです。ソフトアサーションを使うと、1回のテスト実行で複数の問題を同時に発見できます。
import { test, expect } from "@playwright/test";
test("ダッシュボードの表示確認", async ({ page }) => {
await page.goto("/dashboard");
// 失敗してもテストを継続して全項目を確認
await expect.soft(page.getByRole("heading", { level: 1 })).toBeVisible();
await expect.soft(page.getByTestId("user-name")).toContainText("Yushi");
await expect.soft(page.getByTestId("notification-count")).toBeVisible();
// ここまでに蓄積したsoft assertionのエラーが最後にまとめて表示される
});
5. GitHub Actionsで全shardを実行する
旧稿の--shard=1/2だけでは2/2が実行されず、テストが欠落します。全shardをmatrix等で実行します。速度が厳密に半分になるという保証はなく、起動、インストール、テスト配分等に依存します。
以下はsecretを必要としない試験向けの例です。上のログイン試験まで動かすには、専用テストアカウントと認証情報の安全な供給が別途必要です。fork等で使えないときは、全試験済みと扱わず、実行範囲を明示します。個人の本番passwordを渡しません。
name: Frontend Quality Gates
on:
pull_request:
permissions:
contents: read
jobs:
playwright:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2]
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npx playwright install chromium --with-deps
- run: npx playwright test --shard=${{ matrix.shardIndex }}/2
Playwright configのwebServerがbuild/startを行うため、このworkflowでは別のbuildステップを重ねません。Node 20はEOLなので例を24に更新しました。実プロジェクトの互換性とrunner要件を確認します。Actionは確認時点のv6 commitへ固定しています。
Playwright Sharding、Nodeのリリース状態
全shardの失敗を必須statusへ反映し、結果を統合する場合は各shardのblob reportを保存してmergeします。traceやスクリーンショットには機密が含まれ得るため、保存範囲、閲覧権限、期間を定めます。
6. テスト設計のチェックリスト
AIが生成した後は、仕様上の正常・異常・空状態、期待結果、テストデータ、mockと実結合の範囲、全shardの実行、認証状態、失敗時の記録を確認します。CSSを使わないことやPOMの有無だけで品質を判定しません。
本訂正ではコードの構文、workflow YAML、ローカルでのshard分割による試験一覧を確認しました。対象Next.jsアプリのログイン、Storybookのplay、Chromatic、GitHub CIの実行は未検証です。
この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。