【ITニュース解説】Structuring Playwright Tests with the Page Object Model in Python
2026年09月23日に「Dev.to」が公開したITニュース「Structuring Playwright Tests with the Page Object Model in Python」について初心者にもわかりやすく解説しています。
ITニュース概要
Playwrightテストで画面要素の指定や操作が重複し修正が大変になる問題を、Page Object Model (POM) が解決する。POMは画面要素や操作を専用クラスにまとめる設計パターン。これにより、テストコードは簡潔になり、UI変更時の修正が一度で済むため、テストの保守性が大幅に向上する。
ITニュース解説
Webテスト自動化の分野において、アプリケーションの品質保証は非常に重要であり、特にWebアプリケーションのユーザーインターフェース(UI)のテストは、ユーザー体験に直結するため欠かせない工程である。Playwrightは、Microsoftが開発したモダンなWebテスト自動化ライブラリで、Chromium、Firefox、WebKitといった主要なブラウザで動作するテストを簡単に記述できる強力なツールとして広く利用されている。Playwrightを使うことで、ユーザーがブラウザで行う操作(ボタンクリック、テキスト入力、ページの移動など)をプログラムで自動的に実行し、その結果が期待通りであるかを検証できる。
しかし、テストスイート、つまりテストケースの集合が大規模になるにつれて、テストコードの管理が難しくなるという課題に直面することがある。例えば、Playwrightのテストでは、Webページ上の特定の要素を見つけるために「ロケーター」という文字列を使う。これは「このボタンをクリックしてほしい」「この入力欄にテキストを入れてほしい」といった指示をするための、要素の住所のようなものだ。
従来のテストコードでは、これらのロケーターやページ遷移のロジックが、複数のテストファイルの中に散らばって記述されがちである。例として、Googleの検索ボックスが正しく表示されるかをテストするコードを見てみよう。
def test_search_box_ready():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://www.google.com", wait_until="domcontentloaded")
search_box = page.locator("textarea[name='q']")
expect(search_box).to_be_visible()
browser.close()
このコードでは、まずPlaywrightを起動し、Chromeブラウザをヘッドレス(UIが表示されない状態)で立ち上げる。新しいページを開き、Googleのトップページに移動する。そして、page.locator("textarea[name='q']")という部分で検索ボックスのロケーターを直接指定し、それが表示されていることを確認してブラウザを閉じる。
もし複数のテストケースがこの「textarea[name='q']」という同じ検索ボックスを使う場合、そのロケーター文字列がすべてのテストコードの中で繰り返して記述されることになる。この状態が問題となるのは、もしWebページのUIが少し変更され、検索ボックスのロケーター(例えばname='q'がid='search_input'に変わるなど)が変わってしまった場合だ。その変更に対応するためには、ロケーターが記述されているすべてのテストファイルを一つ一つ手作業で探し出し、修正しなければならない。これは非常に手間がかかり、修正漏れが発生するリスクも高まり、結果としてテストコードのメンテナンスコストが跳ね上がる原因となる。
このような問題を解決するために、**Page Object Model(POM)**というデザインパターンが考案された。POMは、Webページ内の要素の構造や、そのページ上での操作方法を、専用の「ページオブジェクト」と呼ばれるクラスに集約してカプセル化する手法だ。これにより、テスト関数自体は、具体的な要素の探し方や操作手順から解放され、純粋に「どのようなユーザーシナリオをテストするのか」というビジネスロジックやテストのフロー、そして最終的なアサーション(検証)にのみ集中できるようになる。
POMを導入した場合のプロジェクト構造は、以下のようにシンプルに整理できる。
project/
pages/
google_page.py
tests/
test_google_pom.py
pagesディレクトリにはWebページのオブジェクトクラスが、testsディレクトリにはそれらのページオブジェクトを利用するテストスクリプトが格納される。
次に、pages/google_page.pyにおけるページクラスの実装を見ていこう。
# pages/google_page.py
from playwright.sync_api import Page, expect
class GooglePage:
URL = "https://www.google.com"
def __init__(self, page: Page):
self.page = page
self.search_box = page.locator("textarea[name='q']")
def open(self):
self.page.goto(self.URL, wait_until="domcontentloaded")
def is_search_ready(self):
expect(self.search_box).to_be_visible()
expect(self.search_box).to_be_editable()
このコードを詳しく見ていく。
from playwright.sync_api import Page, expectは、Playwrightの同期APIから、ページの型ヒント(Page)とアサーションヘルパー(expect)をインポートしている。これにより、コードの可読性と型安全性が向上する。
class GooglePage:は、Googleのホームページを表すページオブジェクトクラスを定義している。このクラスが、Googleページの構造と操作をすべて管理する。
URL = "https://www.google.com"は、対象となるGoogleのURLを定数としてクラス内に保持している。これにより、URLが変更された場合でもこの一箇所を修正すればよくなる。
def __init__(self, page: Page):は、このクラスのコンストラクタであり、初期化時にアクティブなPlaywrightのPageオブジェクトを受け取る。
self.page = pageは、受け取ったPageオブジェクトをクラスのインスタンス変数として保持し、クラス内の他のメソッドで再利用できるようにしている。
self.search_box = page.locator("textarea[name='q']")が非常に重要な部分である。ここでGoogle検索ボックスのロケーターを「一度だけ」定義している。これにより、このロケーターはクラス内で一元管理され、他のテストからはこのGooglePageクラスを通してのみアクセスされるようになる。
def open(self):は、Googleページを開くというナビゲーションステップをカプセル化したメソッドである。
self.page.goto(self.URL, wait_until="domcontentloaded")は、保持しているURLに移動し、ページのDOM(Document Object Model)が完全に読み込まれるまで待機するという操作を実行する。
def is_search_ready(self):は、検索ボックスの準備ができているかを確認する検証ロジックをカプセル化したメソッドである。
expect(self.search_box).to_be_visible()は、検索ボックスが画面上で見えていることを検証するアサーションである。
expect(self.search_box).to_be_editable()は、検索ボックスが入力可能であることを検証するアサーションである。
次に、このページオブジェクトを利用したテストコードtests/test_google_pom.pyを見ていこう。
# tests/test_google_pom.py
from playwright.sync_api import sync_playwright
from pages.google_page import GooglePage
def test_search_box_ready_with_pom():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
google_page = GooglePage(page)
google_page.open()
google_page.is_search_ready()
browser.close()
このテストコードも詳しく見ていこう。
from playwright.sync_api import sync_playwrightは、Playwrightの同期APIのエントリポイントをインポートしている。
from pages.google_page import GooglePageは、先ほど作成したGooglePageクラスをインポートしている。
def test_search_box_ready_with_pom():は、pytestのテスト関数を定義している。
with sync_playwright() as p:は、Playwrightを開始し、テスト終了時に自動的にリソースをクリーンアップするようにしている。
browser = p.chromium.launch(headless=True)は、Chromiumブラウザをヘッドレスモードで起動する。
page = browser.new_page()は、新しいブラウザページを開く。
google_page = GooglePage(page)がPOMの核となる部分である。ここで、新しく開いたpageオブジェクトを引数として渡し、GooglePageのインスタンスを生成している。これにより、このgoogle_pageオブジェクトを通じてGoogleページのすべての操作と要素にアクセスできるようになる。
google_page.open()は、GooglePageクラスに定義されたopenメソッドを呼び出し、Googleページへのナビゲーションを実行する。テストコードは具体的なURLやgotoメソッドを知る必要がない。
google_page.is_search_ready()は、GooglePageクラスに定義されたis_search_readyメソッドを呼び出し、検索ボックスの準備状況を検証する。ここでもテストコードはロケーター文字列を知る必要がない。
browser.close()は、ブラウザセッションを閉じる。
このようにPOMを導入することで、テストコードの可読性は劇的に向上する。テストコードは、まるでユーザーがアプリケーションを操作する手順を記述しているかのように、非常に自然な言葉で読めるようになる。例えば、「Googleページを開いて、検索ボックスが準備できていることを確認する」という具体的な操作が、google_page.open()やgoogle_page.is_search_ready()といったメソッド呼び出しとして表現される。
このパターンがなぜ重要なのかをまとめると、まずロケーターがプロジェクト全体で一箇所にのみ存在するため、UI変更があった際の修正箇所がページクラス内の一箇所に限定される。これにより、メンテナンスコストが大幅に削減される。また、テストコードが具体的なセレクターの記述から解放され、より抽象的で分かりやすい操作フローとして記述できるため、テストの意図が明確になり、可読性が高まる。さらに、新しいテストを作成する際に、既存のページオブジェクトを再利用できるため、コードの重複を避け、開発効率を向上させることができる。大規模なテストスイートを運用する際には、このPOMの利点が特に際立ち、テストコードの品質とメンテナンス性を高いレベルで維持することが可能になる。
結論として、Page Object Modelは、バラバラに散らばりがちなロケーターや繰り返されるナビゲーションコードを、再利用可能な単一の抽象化されたオブジェクトに変換する強力な手段である。これにより、テストスイートが成長してもメンテナンスコストを低く抑え、持続可能なテスト自動化を実現するための鍵となる。