8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ゼロから始めるPlaywright E2Eテスト 〜Claude Codeと共に〜

8
Last updated at Posted at 2025-12-13

本記事は、カオナビ Advent Calendar 2025 (シリーズ 2) 14 日目です。

はじめに

本記事では、Playwright を使った E2E テストをゼロから構築した経験について共有させていただきます。

「E2E テストって何から始めればいいの?」「どんなアーキテクチャで作ればいいの?」といった疑問を持つ方の参考になれば幸いです。

特に、Claude Codeという AI ツールを活用することで、E2E テスト初心者でも効率的にテストを構築できた点をお伝えしたいと思います。

目次

  1. 自己紹介
  2. E2E テスト導入の動機
  3. 技術選定
  4. プロトタイプ作成
  5. 設計・実装方針の策定
  6. Claude Code の活用
  7. 今後の展望
  8. まとめ
  9. 最後に

1. 自己紹介

まず初めに自己紹介をさせていただきます。

私は、ソフトウェア業界で 20 年近い経験を持つ QA エンジニアです。過去にエンタープライズ向けの Windows アプリケーション(サーバは Java、クライアントは Java もしくは WPF)の開発に長く従事しており、商用ツールを使用したテスト自動化プロジェクトにも関ったりしました。コーディング経験は Java か C# をかじった程度、しかもここ 10 年くらいはほとんど書くことはしていませんでした。

そして、今回やろうとしているウェブアプリケーション、Playwright(TypeScript)、AI コーディングについては、すべて未経験の初心者でありました。

2. E2E テスト導入の動機

当初、テスト方針では E2E テストは対応工数などから見送りになっていました。しかし、以下の理由から本格導入を検討することになりました。

  • QA エンジニアのチーム参画 (※私のことです)
  • 基本操作のリグレッションテスト効率化の必要
  • ノーコードツールの使用もしくは Claude Code による実装支援で実現ができそうと判断

3. 技術選定

検討した選択肢

次に、E2E テスト自動化の実現方法として、  Playwright と複数のノーコードテスト作成ツールを比較しました。

項目 Playwright 某 A 社 某 B 社
導入の容易さ ⭕ 社内ネットワーク設定不要 ❌ 社内ネットワーク設定必要 ❌ 社内ネットワーク設定必要
実装の容易さ △ Claude Code で支援 ⭕ ノーコード ⭕ ノーコード
メンテナンス性 ⭕ AI 支援で可能 △ シナリオ毎に修正 △ 柔軟性に欠ける
柔軟性 ⭕ 高い ❌ 低い △ 普通
費用 ⭕ OSS 無料 ❌ 高額 △ 中程度

最終決定:Playwright

厳正なる検討の結果、Playwright を採用することにしました。理由は以下の通りです。

決定理由:

  • プログラミング言語(TypeScript)で記述できる柔軟性
  • OSS 無料、ベンダーロックインなし
  • Claude Code による実装・メンテナンス支援が可能
  • 既存の開発ワークフロー(Git、CI/CD、VS Code)との親和性
  • コーディングが楽しい(※個人的理由)

4. プロトタイプの作成

技術選定の一環として、Playwright でのプロトタイプ作成を並行で進めることにしました。
社内にはすでに Playwright の E2E テスト基盤が存在していましたが、そこをあえてゼロから作成することにしました。その理由は以下の通りです。

  • 既存のテスト基盤は規模が大きいため、全体の把握が困難
  • 適用するアプリケーションが異なるため、ある程度の作り直しが必要
  • コードを読むのもままならない状態なので、ゼロから作成することで学びながら構築できる

先に書いてしまうと、結果としてこのプロトタイプのコードはほぼ作り直しになりました。理由は、大きく 2 つありました。

  1. 再利用性を重視しすぎて、多重構造で複雑な可読性の低いコードになった
  2. (1. の結果として)テスト自動化のアンチパターンをほとんど踏襲してしまった

前職の巨大なエンタープライズアプリケーションの Java コードが頭にあって、部品化を意識しすぎたのだと思います。

幸いにも、プロジェクト的にはこの試行錯誤期間を許容する余裕がありましたので、アンチパターンがなぜダメなのか、ということを身をもって知ることができて、大変有益でした。

5. 設計・実装方針の策定

Playwright で行くという方針が決まりましたので、次は設計・実装方針の策定です。
プロトタイプの失敗を踏まえて、採用にあたっては以下のルールを定義しました。

  1. Playwright 公式の Best Practices に準拠する
  2. DRY と DAMP のバランスを意識する

5.1 アーキテクチャパターン

5.1.1 Page Object Model(POM)

採用理由:

Page Object Model(POM)は、UI の構造をクラスとしてカプセル化し、テストコードから UI 実装の詳細を隠蔽するデザインパターンです。このパターンを採用した主な理由は以下の 3 点です。

  1. UI 変更への耐性向上: UI の変更時、Page Object クラスのみを修正すれば全テストに反映される。テストコード自体の修正が不要になり、保守コストが大幅に削減される
  2. テストコードの可読性向上: テストコードから実装詳細(セレクター、待機処理など)が排除され、ビジネスロジックに集中できる
  3. DRY 原則の実現: 同じページに対する操作を複数のテストで再利用でき、コードの重複を防ぐ
// tests/pages/ProductCreatePage.ts
export class ProductCreatePage {
  readonly page: Page;
  readonly heading: Locator;
  readonly productNameInput: Locator;
  readonly descriptionTextArea: Locator;
  readonly registerButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.heading = page.getByRole("heading", { name: "製品の登録" });
    this.productNameInput = page.getByRole("textbox", { name: "製品名" });
    this.descriptionTextArea = page.getByRole("textbox", { name: "説明" });
    this.registerButton = page.getByRole("button", { name: "登録" });
  }

  async fillProductName(productName: string) {
    await this.productNameInput.fill(productName);
  }

  async fillDescription(description: string) {
    await this.descriptionTextArea.fill(description);
  }

  async clickRegister() {
    await this.registerButton.click();
  }
}

実装のポイント:

  • Locator はgetByRole()などのセマンティックなメソッドを優先
  • Page Object はアクションのみを提供し、アサーションはテスト側で実施

5.1.2 Component-Based Architecture

採用理由:

複数のページで共通する UI コンポーネント(ヘッダーナビゲーション、サイドバー、フッターなど)を独立したクラスで管理するアーキテクチャです。このパターンを採用した主な理由は以下の 2 点です。

  1. 共通 UI ロジックの一元管理: 同じ UI コンポーネントに対する操作が複数の Page Object に散在することを防ぎ、変更時の修正箇所を 1 箇所に集約できる
  2. 変更の影響範囲の最小化: ナビゲーション構造の変更(メニュー項目の追加・削除など)が発生しても、コンポーネントクラスのみを修正すれば全テストに反映される
// tests/components/HeaderNavigation.ts
export class HeaderNavigation {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async goToProductListPage() {
    await this.page.getByRole("link", { name: "製品" }).click();
  }

  async goToCustomerListPage() {
    await this.page.getByRole("link", { name: "顧客" }).click();
  }

  async goToOrderListPage() {
    await this.page.getByRole("link", { name: "注文" }).click();
  }
}

5.1.3 Test Fixtures Pattern

採用理由:

Test Fixtures は、テストの前提条件(Setup)とクリーンアップ(Teardown)を自動化する仕組みです。このパターンを採用した主な理由は以下の 3 点です。

  1. テストの独立性の確保: 各テストが独自のテストデータを持ち、他のテストに依存しない。これにより並列実行が可能になり、テストの実行順序に依存しない堅牢なテストスイートを構築できる
  2. セットアップ/クリーンアップの自動化: データ準備とクリーンアップのコードがテストコードから分離され、テストの可読性が向上する。また、テスト失敗時でもクリーンアップが確実に実行される
  3. 並列実行の実現: workerIndex とタイムスタンプを組み合わせたユニークなデータ生成により、複数のテストが同時実行してもデータの衝突が発生しない
// tests/fixtures.ts
type MyFixtures = {
  loggedInPage: { page: Page; username: string };
  productTestData: {
    productName: string;
    category: string;
  };
};

export const test = base.extend<MyFixtures>({
  loggedInPage: async ({ page }, use) => {
    const username = process.env.USER_NAME;
    const password = process.env.PASSWORD;

    const authPage = new AuthPage(page);
    await authPage.goto();
    await authPage.login(username, password);

    await use({ page, username });
  },

  productTestData: async ({ page }, use, testInfo) => {
    const timestamp = Date.now();
    const workerIndex = testInfo.workerIndex;
    const productName = `E2Eテスト製品_${timestamp}_${workerIndex}`;
    const category = "テストカテゴリ";

    // Setup: カテゴリやその他の前提データを事前登録
    const productPage = new ProductCreatePage(page);
    await productPage.goto();
    await productPage.fillProductName(productName);
    await productPage.fillCategory(category);
    await productPage.clickRegister();
    
    await use({ productName, category });
    
    // Teardown: テスト後にデータ削除
    const productListPage = new ProductListPage(page);
    await productListPage.goto();
    await productListPage.selectProduct(productName);
    const productDetailPage = new ProductDetailPage(page);
    await productDetailPage.deleteProduct();
  },
});

Fixture の責務:

  • Setup: テスト実行前のデータ準備
  • データ提供: テストに必要なデータを提供
  • Teardown: テスト後のクリーンアップ

5.2 実装方針

5.2.1 ユーザーストーリーベースのテスト分割

採用理由:

各テストファイルを単一のユーザーストーリーに対応させるアプローチです。このパターンを採用した主な理由は以下の 3 点です。

  1. テストピラミッドとの整合性: E2E テストを「主要なユーザーフローを最低限カバーする」ものと再定義し、単体テストや結合テストとの役割分担を明確化。テストの巨大化を防ぎ、適切な粒度を保つ
  2. CI 効率化とフィードバックループの高速化: テストスクリプトが小さくなることで実行時間が短縮され、開発者へのフィードバックが早くなる。失敗時の原因特定も容易になる
  3. デバッグの容易性: テストファイル名から検証内容が即座に理解でき、PRD のユーザーストーリーとの対応関係が明確。変更時の影響範囲も把握しやすい
tests/
├── login-logout.test.ts            # ログイン・ログアウト
├── product-create-required.test.ts # 製品登録(必須項目)
├── product-create-all.test.ts      # 製品登録(全項目)
├── customer-create-required.test.ts # 顧客登録(必須項目)
└── order-create-required.test.ts   # 注文登録(必須項目)

5.2.2 test.step による段階的検証

採用理由:

各テストを複数の step に分割し、1 step = 1 検証目的 の原則に従うアプローチです。このパターンを採用した主な理由は以下の 3 点です。

  1. 失敗箇所の即座の特定: テストが失敗した際、どの step で失敗したかが明確になり、デバッグ時間を大幅に短縮できる。step 名から失敗の原因を推測しやすい
  2. テストレポートの可読性向上: Playwright Report で各 step の実行結果(スクリーンショット、トレースなど)を個別に確認でき、非エンジニアでも理解しやすいレポートになる
  3. テストの段階的な進行の可視化: ユーザーフローの各ステップが明示的になり、どこまで正常に動作したかが一目で分かる。部分的な成功・失敗の把握が容易になる
test("顧客登録(必須項目)の動作確認", async ({
  loggedInPage,
  customerTestData,
}) => {
  const page = loggedInPage.page;
  const { customerName, email, phoneNumber } = customerTestData;

  await test.step("ヘッダーナビゲーションから顧客一覧ページが表示されていること", async () => {
    const headerNavigation = new HeaderNavigation(page);
    await headerNavigation.goToCustomerListPage();
    const customerListPage = new CustomerListPage(page);
    await expect(customerListPage.heading).toBeVisible();
  });

  await test.step("顧客の登録ボタンをクリック後、顧客の登録ページが表示されていること", async () => {
    const customerListPage = new CustomerListPage(page);
    await customerListPage.clickCustomerCreateButton();
    const customerCreatePage = new CustomerCreatePage(page);
    await expect(customerCreatePage.heading).toBeVisible();
  });

  await test.step("必須項目を入力して登録ボタンをクリック後、顧客詳細ページが表示されていること", async () => {
    const customerCreatePage = new CustomerCreatePage(page);
    await customerCreatePage.fillCustomerName(customerName);
    await customerCreatePage.fillEmail(email);
    await customerCreatePage.fillPhoneNumber(phoneNumber);
    await customerCreatePage.clickRegister();
    const customerDetailPage = new CustomerDetailPage(page);
    await expect(customerDetailPage.heading).toBeVisible();
  });

  await test.step("顧客詳細ページで、顧客名が表示されていること", async () => {
    const customerDetailPage = new CustomerDetailPage(page);
    await expect(
      customerDetailPage.getCustomerNameHeading(customerName)
    ).toBeVisible();
  });

  await test.step("顧客詳細ページで、メールアドレスが表示されていること", async () => {
    const customerDetailPage = new CustomerDetailPage(page);
    await expect(
      customerDetailPage.getInformationItem("メールアドレス")
    ).toContainText(email);
  });

  // ... 以降のstepも同様に ...
});

メリット:

  • 各 step が何を検証しているか一目瞭然
  • 失敗した step を即座に特定可能
  • Playwright Report で各 step の結果を確認できる

6. Claude Code の活用方法

そして忘れてはならないのが、Claude Codeの支援です。E2E テスト初心者の私がここまで実装できたのは、間違いなく AI エージェントの存在があったからです。

6.1 Claude Code とは

Claude Code は Anthropic が提供する AI 開発支援ツールで、コードの生成・レビュー・リファクタリングを支援します。

6.2 活用シーン

基本的には、Claude Code で仮実装を行い、生成したコードを確認しながら手動で修正もしくは Claude Code に問題点を指摘して修正してもらう、というサイクルを繰り返していきました。

また、ちょっとした修正やリファクタリングを Claude Code に任せることで、広範囲に対して修正漏れといった単純ミスやコーディング規約違反を防ぐこともでき、正直 Claude Code なしではこのスピードと品質で E2E テストを構築できなかったと断言できます。

Claude Code が生成したコードをある程度は読めないと難しいところはありますが、幸いコードを読むことについてはかつて Java などで経験していたので、慣れとともにスムーズにできるようになりました。

なお、ちょっとしたハマりポイントもありましたので、記録残しも兼ねて以下に挙げてみます。

根拠の提示:

実装方法に迷ったとき、Playwright の Best Practice に準じているかどうか、を調べた上で提案してもらうのですが、「準拠している」と回答した内容をよくよく調べてみると、実は根拠がなく準拠していなかった、ということが多々ありました。

ですので、必ず「準拠しているという根拠(ページの URL)を提示すること」というルールを追加して再発防止をしています。

問題の調査:

何かしらでテスト実行が止まったときに、調査をお願いするのですが、原因がわからないとアンチパターンをワークアラウンドとして勧めてくることが多かったです。これまでの経験では、テスト実行が止まる場合、コード側の不備の修正(操作の順序がおかしい、同じページを 2 度読み込んでいる、など)か、page.waitForURL()を使用することで解決していました。

Claude Code はpage.waitForTimeout(5000)のような固定待機時間や、test.setTimeout()でタイムアウト値を増やすといった甘〜い誘惑を提案してくることが多いので、グッと堪えて根気強く原因を調べましょう。

7. 今後の展望

まだテストスクリプトの本数は少ないですが、CI で定期実行するところまでできました。(クロスブラウザも実現)

今後は、継続して基本機能のテストスクリプトを拡充しつつ、アクセシビリティテストやビジュアルリグレッションテストといったものにもチャレンジしていこうと考えています。

8. まとめ

E2E テストの作成経験のない QA エンジニアである私が、Playwright と Claude Code を活用して E2E テスト基盤をゼロから構築した経験を共有させていただきました。

重要な学び

1. プロトタイプでの失敗が貴重な学びに

  • 再利用性を重視しすぎた過度な部品化はアンチパターン
  • DRY と DAMP のバランスが重要
  • 失敗を通じて「なぜダメなのか」を体験できた

2. Best Practices への準拠が成功の鍵

  • Playwright 公式の Best Practices に従うことでフレーキネスを最小化
  • セマンティックな Locator の使用(getByRole()など)
  • Web-first assertions による自動待機

3. 設計・実装方針の適切な選択

  • Page Object Model: UI 変更への耐性、テストコードの可読性向上
  • Component-Based: 共通 UI ロジックの一元管理
  • Test Fixtures: テストの独立性と並列実行の実現
  • ユーザーストーリーベース: テストの適切な粒度とデバッグの容易性
  • test.step: 失敗箇所の即座の特定、レポートの可読性向上

4. Claude Code の効果的な活用

  • コード生成だけでなく、学習のパートナーとして機能
  • 初心者でも品質の高いコーディングが可能
  • その一方、実装案や生成されたコードのすべてが正しいわけではないことを認識する

今後 E2E テストを始める方へのアドバイス

  1. 小さく始める: まずはプロトタイプで試行錯誤を
  2. 公式ドキュメントを信頼する: Best Practices には理由がある
  3. AI を味方につける: Claude Code は強力なパートナー
  4. 失敗を恐れない: アンチパターンを踏むことも学びの一部

この記事が、これから E2E テストを始める方の参考になれば幸いです。

9. 最後に

ここまでかかった期間は技術選定・プロトタイプ約 3 ヶ月、E2E 基盤をプロダクトのリポジトリに載せて CI 実行までが約 2 ヶ月です。

もちろん、私一人でやれたわけではありません。方針策定やレビューはチーム内のみならずチーム外の有識者の方にも関わっていただいていますし、CI 実行はインフラエンジニアの方にすべて対応いただきました。

この場を借りて、ご協力いただいたすべての方にお礼申し上げます。

参考資料


8
0
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
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?