🤖

Page Object ModelがAIエージェント(Claude Code等)と相性が良い理由

に公開

はじめに

Claude CodeやCodexなどのAIエージェントにはE2Eテストを書くときも大変お世話になっています。

ある時ふと、Page Object Model(POM)ってAIエージェントと相性が良いのでは? と思いました。ファイルが分割されている、パターン化できる、など、AIエージェントが扱いやすそうな特徴が揃っているからです。

実際に試してみていい感じだったので、その理由と実践方法を紹介します。


Page Object Modelとは

Page Object Modelは、E2Eテストにおけるデザインパターンの一つです。ページごとにクラスを作成し、そのページの要素やURLをカプセル化します。

tests/e2e/
├── _pages/           # Page Objectファイル
│   ├── base.page.ts  # 基底クラス(共通要素)
│   ├── agents.page.ts
│   ├── chat.page.ts
│   └── ...
└── *.spec.ts         # テストファイル

テストファイルからは、Page Objectを通じて要素にアクセスします。


POMがAIエージェントと相性が良い4つの理由

1. ファイル分割でコンテキストを最小化

AIエージェントは大きいファイルと相性が悪いです。コンテキストが大きくなると、回答の精度が下がったり、処理に時間がかかったりします。

POMを採用すると、「Page Objectファイル」と「テストファイル」を分離できます。AIエージェントは必要なファイルだけを参照/更新できるようになります。

2. パターン化で既存コードへの追従が容易(レールを敷ける)

AIエージェントは、既存コードがきれいにパターン化されていると、それに従ったコードをうまく生成してくれる印象があります。

POMを使うことでE2Eテストのパターン(レール)をAIエージェントに示すことができるので、既存テストと一貫したコードを生成してくれやすくなります。

export class AgentsPage extends BasePage {
  get createButton() {
    return this.page.getByRole('button', { name: '新規作成' });
  }
  get nameInput() {
    return this.page.getByLabel('名前');
  }
}

「既存のPage Objectを参考に新しいPage Objectを作って」と指示すれば、既存のメソッド定義を真似したコードを生成してくれます。

3. POMがテストの「目次」になる

POMの最も実用的な利点は、Page Objectを見るだけでそのページにどんな要素があるか分かることです。

export class AgentsPage extends BasePage {
  get pageTitle() { ... }
  get createButton() { ... }
  get nameInput() { ... }
  get saveButton() { ... }
  agentCard(name: string) { ... }
  agentEditButton(name: string) { ... }
  agentDeleteButton(name: string) { ... }
}

このPage Objectを見れば、「作成ボタン」「名前入力欄」「編集・削除ボタン」などがあることが一目瞭然です。AIエージェントに新しいページのテスト作成を依頼すると、既存のPage Objectから似た要素を持つページを見つけ、参考にすべきテストを判断してくれることが多いです。テストファイル全体を見る必要がありません。

実際にClaude Codeを使っている感触としては、ほとんどの場合こちらからPage Objectを明示的に指定しなくても、必要に応じて自ら参照してくれています。

4. 管理コストもAIに任せられる

POMのデメリットとして「Page Objectの管理コスト」がよく挙げられます。

しかし、AIエージェントとの開発ではこの管理コストをあまり気にしなくて良くなります。UIを変更すれば基本的にPage Objectも一緒に更新してくれますし、そうでなくても「Page Objectを更新して」と依頼するだけで済みます。


実践例

プロンプト例

「既存のテストを参考に、設定画面でタイムゾーンを変更するテストを追加してください」

タイムゾーンの変更はセレクトボックスで実装されているとします。このような依頼をすると、AIエージェントは以下のような流れで作業を進めてくれます。

  1. Page Objectを探索して、セレクトボックスを持つページを見つける
  2. そのページのテストファイルを参照して、セレクトボックス操作の書き方を把握する
  3. 既存のパターンに倣って新しいテストを作成する

CLAUDE.mdへの設定例

プロジェクトのルールとしてCLAUDE.mdに以下のような設定しておくと、AIエージェントが自然とPOMのパターンに従ってくれます。

## E2Eテスト

- Page Objectは `tests/e2e/_pages/` に配置
- テストファイルは `tests/e2e/*.spec.ts` に配置
- 新規テスト追加時は既存のPage Objectを確認し、必要に応じてメソッドを追加
- 新規ページのテストはBasePageを継承してPage Objectを作成
- Page Objectには要素を返すgetter/メソッドのみを定義し、複雑なロジックは持たせない

POMの設計指針

POMを効果的に活用するために、設計時に意識しているポイントがあります。

原則: POMは「要素の抽象化」に専念する

Page Objectには、要素を返すgetterやメソッドのみを定義します。複数ステップの操作ロジックを持たないようにしています。

理由:

  • POMにロジックを持たせすぎると、テストファイルだけでは操作の流れが追えなくなり、テストの意図がわかりづらくなる
  • POMファイルが肥大化し、AIエージェントに渡すコンテキストが大きくなってしまう(本末転倒)

良い例

export class AgentsPage extends BasePage {
  // 要素を返すgetterのみ - 何があるかが一目瞭然
  get createButton() {
    return this.page.getByRole('button', { name: '新規作成' });
  }

  get nameInput() {
    return this.agentFormDialog.getByLabel('名前');
  }

  // メソッドも要素を返すだけ
  agentCard(name: string) {
    return this.page.getByTestId('agent-card').filter({ hasText: name });
  }

  agentDeleteButton(name: string) {
    return this.agentCard(name).getByRole('button', { name: '削除' });
  }
}

実用上の例外

とはいえ、ほぼ全テストで使う前提条件系の操作(signIn等)やテストの可読性が落ちる複雑な操作はメソッドとして定義しても良いと考えています。

// base.page.ts
export abstract class BasePage {
  // ほぼ全テストで使う前提条件 - これは許容範囲
  async signIn(email: string, password: string) {
    await this.page.goto('/sign-in');
    await this.page.getByLabel('メールアドレス').fill(email);
    await this.page.getByLabel('パスワード').fill(password);
    await this.page.getByRole('button', { name: 'ログイン' }).click();
    await expect(this.page.getByText('ログインしました')).toBeVisible();
  }
}

判断基準は「テストファイルだけ見て何をしているか分かるか?」です。

なお、これは現時点でAIエージェントと人間が共同で開発するという前提での指針です。AIエージェントの進化に伴い、この判断基準は変わっていくかもしれません。


おわりに

「POMってAIエージェントと相性が良いのでは?」という仮説を試してみた結果、今のところAIエージェントと良い関係が築けているように思います。

AIエージェントとE2Eテストを書く機会がある方は、POMを試してみてはいかがでしょうか。


この記事はZenn/Qiitaにクロスポストしています

Discussion