1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI生成テストを検証する:Playwrightの全shard実行とStorybookの品質ゲート

1
Last updated at Posted at 2026-02-20

訂正(2026-10-12): 欠落していたCIの全shard実行、認証状態、Storybookの検証範囲、ロケータ方針を修正しました。以下はテスト設計例で、特定のNext.jsアプリやStorybook全体の動作確認済み実装ではありません。

はじめに

Vibe Codingでコードの生成速度が上がるほど、人間が担保すべきは「振る舞いの定義(仕様)」=テストコードにシフトします。ところが皮肉なことに、AIにテストも「ただ書かせる」だけでは、CSSセレクタに依存した壊れやすいテストが量産されてしまいます。

本記事では以下の3つの問いに答えます。

  1. E2Eテスト(Playwright) をAIと正しく協働して生成するにはどうすればよいか?
  2. Storybookを使った表示・操作・視覚差分の検証をどう分けるか?
  3. 「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等に固定し、個人の認証済みセッションではなく専用のテスト環境を使います。

Playwright MCP公式

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しません。サーバーのデータを変更する並列試験では、アカウントやデータの衝突も避けます。

Playwright認証

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禁止はチーム独自の方針にできても、公式の一律必須規約ではありません。

Playwright Locators

// ❌ 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で発信しています。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?