🤖

POMを使ったSeleniumの実践メモ

2025/04/15に公開

📌 TL;DR

この記事は動的ページの自動テストに、POM(Page Object Model)設計を使って効率的な実装を試してみた話です。

  • 目的:
    動的な React ベースの画面に対する自動 UI テストの信頼性と保守性を上げたい。
  • 工夫ポイント:
    開発時に要素の命名規則とテスト用の属性(data-testid)、意味のある ID を設計時に組み込むよう意識しておくことで、テストコードの構造がシンプルになり保守が楽に。
    ExpectedConditions を組み込んだ共通関数を作っておくことで、POMクラスの再利用性が高まる。

Page Object Model(POM)とは?

POMとは、テスト対象となるページごとに「操作(アクション)」と「要素(ロケータ)」をクラスとしてまとめる設計パターンです。
Selenium を使った自動テストにおいて、UIの変更に強く、再利用性の高いテストコードを実現できるのが特徴です。

  • メリット
    テストコードの可読性と保守性が向上する
    ページごとの処理がクラスで分離され、整理しやすい
    再利用しやすく、スケーラブルなテスト設計が可能になる

  • デメリット
    初期設計にある程度の学習コストと設計工数がかかる
    少量のテストケースではややオーバーエンジニアリングになりがち

それでも、ページ構成が複雑だったり、今後テストケースが増加することが見込まれるプロジェクトではPOMは非常に有効な手段となります。

💡 開発課題と選定理由

今回テスト対象としたのは、要素のコンポネートがボタン制御で動的に切り替わる Web アプリケーションでした。フロントエンド側では、検索条件に応じて表示が変わったり、非同期でデータが更新される仕組みが多く組み込まれていて、

「ページ遷移や要素の表示タイミングによってテストが不安定になる」
「一部の要素がまだ読み込まれていない状態でクリックしてしまう」

といった、動的要素特有の課題がありました。

それで POM 設計パターンを採用して、React の製造でも自動テストを意識しつつテストの信頼性とメンテナンス性を両立させる方法に挑戦してみました。

React コンポーネントとテスト自動化の連携

React では、コンポーネントが動的に生成されるため、要素の識別子(IDやクラス名)が一貫していない場合があります。これにより、Selenium での要素取得が困難になることがあります。例えば、毎回異なる ID が生成される要素に対しては、固定のセレクタでの取得が難しくなります。

この課題に対処するため、開発時に以下の工夫を行いました。

  • 静的要素に固定の ID を付与:テスト対象の要素には、開発段階で一貫した ID を付与することで、Selenium からのアクセスを容易にした。
  • 動的要素に data 属性の活用:data-testid などのカスタム属性を使用して、テスト専用の識別子を設けて、UI の変更に影響されずに要素を特定できた。
  • クラス名の命名規則:クラス名には一貫した命名規則を適用し、必要に応じて BEM を取り入れた。

識別子の分け方

種類 対象 識別方法 理由
静的要素 ラベル、ボタン、入力欄など By.ID or By.NAME HTML構造に直接固定されていて変更が少ないため
動的要素 .map()で生成された要素リスト By.CSS_SELECTOR + data-testid クラス名や構造が変化しやすく、IDが重複しやすいため安全に分離可能

具体例として

例えば、React で以下のような動的要素があります。

{tableList.map((table) => (
  <div key={table.id} data-testid={`table-option-${table.id}`}>
    {table.item}
  </div>
))}

ロケータをこんなふうに定義します。

class TableListLocators:
    @staticmethod
    def table_option_by_id(table_id):
        return (By.CSS_SELECTOR, f"[data-testid='table-option-{table_id}']")

ページクラスではこう書きました。

# 要素の表示状態を確認
def check_if_displayed(self, locator):
    return self.wait.until(EC.visibility_of_element_located(locator))

# 特定要素を選択
def select_table_option(self, table_id):
    locator = TableListLocators.table_option_by_id(table_id)
    option = self.check_if_displayed(locator)
    option.click()

✏️ 全体の流れ

フローはこんな感じでした。

POM 設計を使った感想

POM 設計をやってよかった!
固定の ID やテスト用の一貫したクラス名があることで、POM クラス内での要素定義が簡潔になり、コードの再利用性が高まり、ページ固有の操作はページクラスやその派生クラスに分けて、テストシナリオに基づいた各操作がメソッドとして明確に定義しました。
例えばクリック操作、表示制御時の状態確認、テキスト取得など、1メソッド1機能が徹底していくことでテストコードの保守コストも抑えられ、React の仮想 DOM は、POM 設計とは相性がよいなあと実感しました。

しかし一貫した設計文化が定着させるためにBEMを取り入れたけど、機能が複雑であればであるほどクラス名が長くなってしまいます。。今後の改善点として、役割(動詞)と状態のみに絞って簡略化するとか、命名規則を見直そうと考えています。

Discussion