ホーム自己紹介ブログ
NO.208
DATE2025. 12. 26

Playwright の POM を Storybook 上で確認してから E2E テストを書く

はじめに

Playwright で E2E テストを書く際、playwright codegen や、近年では Playwright MCP を利用して、テストコードの雛形を作成することが多いと思います。

ただし、生成したテストコードが正しく動作するかどうかは、実際に E2E テストを実行するまで確認できません。

E2E テストは、単体テストなどと比べて、実行環境の準備が重く、実行時間が長くなりやすい傾向があります。また、実行環境の影響を受けやすく、テストが不安定になる場合もあります。

そのため、テストコードを書いてから動作確認を行うまでの往復に、想像以上の時間がかかることがあります。

Storybook を使った事前検証という考え方

この課題に対して、私は Playwright のテストコードを、最初から実アプリケーションに向けて実行するのではなく、Storybook をテスト対象として利用する方法を取っています。

Storybook では、ページ単位のコンポーネントを描画対象とします。 そのページ単位のコンポーネントの近くにPage Object Model(POM)を配置し、まずは Storybook 上で POM の動作確認を行います。

この段階の目的は、E2E テストを書くことではありません。 POM が意図した要素を取得できるか、想定した操作が成立するかを、軽量な環境で確認することにあります。

本記事で扱う実例について

本記事で紹介しているテスト構成は、個人で開発している Web サービス、Fequestの実装をもとにしています。

Fequest
ほしいとつくるを共有するプラットフォーム
fequest.vercel.app

Fequest は Feature Request の略で、 プロダクトに対して機能リクエストを送れるサービスです。

ほしいとつくるを共有するプラットフォーム
ユーザーがほしい機能をリクエストし、開発者がそれをつくるにつなげる、
みんなでプロダクトを育てる場所です。

Fequest 自体にも機能リクエストが送れます。プロダクト登録も誰でもできます。

Fequest | Fequest
ほしいとつくるを共有するプラットフォーム。 ユーザーがほしい機能をリクエストし、開発者がそれをつくるにつなげる、みんなでプロダクトを育てる場所です。
fequest.vercel.app

このプロダクトでは、今回紹介する Storybook と Playwright を組み合わせた POM の検証方法や、Cucumber と Testcontainers を用いた E2E テスト構成を実際に採用しています。

ソースコードは公開しており、テスト周りの実装も含めて確認できます。

GitHub - silverbirder/fequest
Contribute to silverbirder/fequest development by creating an account on GitHub.
github.com

フォルダ構成の例

この構成を前提としたフォルダ構成は、以下のようになります。

src/
├─ components/
│  ├─ Product.tsx
│  └─ Product.stories.tsx
├─ e2e/
│  ├─ product.page.ts
│  └─ product.page.spec.ts
├─ features/
│  ├─ product.feature.md
│  └─ product.steps.ts
├─ playwright.config.ts
├─ cucumber.config.js
└─ package.json

ページ単位コンポーネントの例

以下は、管理画面のプロダクトページを想定した、ページ単位コンポーネントの例です。

// src/components/Product.tsx
type Props = {
  product: {
    name: string;
    description?: string;
    features: { id: number; title: string }[];
  };
  onDelete?: () => void;
};
 
export function Product({ product, onDelete }: Props) {
  return (
    <div>
      <h1>プロダクトの管理</h1>
 
      <section>
        <h2>{product.name}</h2>
        {product.description && <p>{product.description}</p>}
      </section>
 
      <ul>
        {product.features.length === 0 ? (
          <li>リクエストはまだありません</li>
        ) : (
          product.features.map((feature) => (
            <li key={feature.id}>{feature.title}</li>
          ))
        )}
      </ul>
 
      <button onClick={onDelete}>
        プロダクトを削除
      </button>
    </div>
  );
}

Storybook の用意

上記のコンポーネントに対して、表示と操作確認に必要な最小限の Storybook を用意します。

// src/components/Product.stories.tsx
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { Product } from "./Product";
 
const meta = {
  title: "Feature/Product",
  component: Product,
  args: {
    product: {
      id: 1,
      name: "サンプルプロダクト",
      description: "ユーザーからの要望を集めるプロダクトです。",
      features: [
        { id: 1, title: "プロフィール画像アップロード" },
        { id: 2, title: "管理画面のフィルタ機能" },
      ],
    },
    onDelete: async () => {
      console.log("delete product");
    },
  },
} satisfies Meta<typeof Product>;
 
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

Page Object Model の実装

ページ単位コンポーネントに対応する POM を実装します。 以下は、表示確認と基本操作のみを持つ簡易的な例です。

// src/e2e/product.page.ts
import { expect, type Page } from "@playwright/test";
 
export class ProductPage {
  constructor(private page: Page) {}
 
  async goto(url: string) {
    await this.page.goto(url, { waitUntil: "networkidle" });
  }
 
  async expectVisible() {
    await expect(
      this.page.getByRole("heading", { name: "プロダクトの管理" }),
    ).toBeVisible();
  }
 
  async deleteProduct() {
    await this.page.getByRole("button", { name: "プロダクトを削除" }).click();
    await this.page.waitForLoadState("networkidle");
  }
}

この POM は、Storybook と実アプリケーションのどちらのテストからも再利用できます。

Storybook を対象とした Playwright テスト

まずは Storybook を対象に、POM の動作確認を行います。

// src/e2e/product.page.spec.ts
import { test } from "@playwright/test";
import { ProductPage } from "./product.page";
 
test("product page (storybook)", async ({ page }) => {
  const productPage = new ProductPage(page);
 
  await productPage.goto(
    "/iframe.html?id=feature-product--default",
  );
 
  await productPage.expectVisible();
});

Playwright と Storybook の設定

Storybook を起動した状態で、Playwright のテストを実行するための設定例です。

{
  "scripts": {
    "storybook:e2e": "STORYBOOK_PORT=6000 playwright test"
  },
  "devDependencies": {
    "@playwright/test": "~1.57.0",
    "playwright": "~1.57.0",
    "@storybook/nextjs-vite": "^10.1.10"
  }
}
// playwright.config.ts
import { defineConfig } from "@playwright/test";
 
const port = Number(process.env.STORYBOOK_PORT ?? 6000);
 
export default defineConfig({
  testDir: "./src/e2e",
  use: {
    baseURL: `http://localhost:${port}`,
    viewport: { width: 1280, height: 720 },
  },
  webServer: {
    command: `storybook dev -p ${port}`,
    port,
    reuseExistingServer: true,
    timeout: 120_000,
  },
});

E2E テストへの展開

Storybook 上で POM の動作確認ができたら、次に実アプリケーションを対象としたE2E テストを記述します。

私は、受け入れ駆動と使い捨てE2E環境が好みなので、Cucumber と Testcontainers を組み合わせています。

シナリオ: プロダクト管理画面を表示できる
  前提 管理画面が起動している
  もし プロダクト管理画面を開く
  ならば プロダクト管理画面が表示される
// src/features/product.steps.ts
import { Given, When, Then } from "@cucumber/cucumber";
import { ProductPage } from "../e2e/product.page";
 
let productPage: ProductPage;
 
Given("管理画面が起動している", async function () {
  // Testcontainers で adminUrl, page が用意済み
  productPage = new ProductPage(this.page);
});
 
When("プロダクト管理画面を開く", async function () {
  await productPage.goto(
    `${this.adminUrl}/products/${this.productId}`,
  );
});
 
Then("プロダクト管理画面が表示される", async function () {
  await productPage.expectVisible();
});

おわりに

この構成では、POM の検証と E2E テストの記述を分離できます。

Storybook を中間地点として利用することで、E2E テストに進む前の不確実性を抑えた状態で、テストコードを書くことができます。 ぜひ、参考にしてみてください。

テスト

-

シェアする

フォローする

次のページ

Web配色とoklch設計

前のページ

2025年仕事の思い出:1年間、週4在宅ワークやってみた

関連する記事

タグ「テスト」の記事

CSS Layout Testing というテスト手法の提案

Web のフロントエンド実装において、次のようなミスによってデザイン崩れを起こしてしまったことはありませんか。 flex-shrink の指定を忘れて、要素が押しつぶされてしまった z-index の指定を間違えて、要素が意図せず前面(また

2026-01-10

フロントエンド
テスト
単体テストを全通り書くんじゃない!

AIの進化によって、プロダクションコードに対するテストコードは、以前と比べて格段に書きやすくなったと感じています。 単体テストに関する基本的なお作法については、以前に以下の記事で整理しました。 興味があれば、参考として読んでもらえると嬉しい

2026-01-09

テスト
Storybook の Story を Vitest Browser Mode with Playwright で VRT

Storybook の Story オブジェクトを Vitest の Browser Mode だけで、Visual Regression Test(VRT)ができるようになりました。 本記事では、その導入手順をコンパクトに紹介します。 前

2025-12-03

フロントエンド
テスト
← ブログ一覧へ