Page Object Model(POM)の基本は理解したものの、実際のプロジェクトで運用すると「毎回 Page Object をインスタンス化するのが面倒」「ヘッダーやモーダルなど共通UIのコードが各ページで重複する」「ログイン処理が遅くてテスト全体の実行時間が伸びる」といった壁にぶつかることがあります。この記事では、pytest の Fixtures・Component Object パターン・storage_state を組み合わせて、基本のPOMをさらに一段引き上げる応用テクニックを解説します。
POMの「ページごとにクラスを作る」という基本原則だけでは、テストが増えるにつれて別の課題が出てきます。Fixturesとの連携設計が、実務での運用効率を大きく左右します。
📌 この記事はこんな方におすすめ
- Page Object Model の基本(ページごとのクラス設計)は実践済みの方
- 各テストで
LoginPage(page)のようなインスタンス化を毎回書くのが冗長だと感じている方 - ヘッダー・モーダルなど複数ページで共通するUIのコードが重複している方
- ログイン処理の実行時間がテストスイート全体を遅くしていると感じている方
✅ この記事を読むと得られること
- pytest Fixture で Page Object を自動注入し、テストコードからボイラープレートを消す方法がわかる
- Component Object パターンで共通UIの重複を解消する設計がわかる
storage_stateを使って認証済み状態を再利用し、テスト実行時間を短縮する方法がわかる
👤 この記事を書いた人
QAエンジニア・テスト自動化エンジニアとして15年以上の実務経験を持つ Yoshi が執筆。Playwright × pytest でのテスト基盤構築を複数プロジェクトで担当し、Page Object Model の運用設計を実務で継続的に改善してきました。
📌 結論
- Page Object は pytest Fixture で自動注入すると、テストコードから初期化処理が消えて読みやすくなる
- ヘッダーなど複数ページで共通するUIは Component Object として切り出すと、重複と肥大化を防げる
storage_stateで認証済み状態を再利用すれば、ログイン処理の繰り返しによる実行時間増加を抑えられる
基本の Page Object Model を導入したプロジェクトが成長すると、次のような課題が見え始めます。
- すべてのテストで
LoginPage(page)・DashboardPage(page)のようなインスタンス化コードが繰り返される - ヘッダー・サイドバー・通知モーダルなど、複数ページに共通するUIの操作コードが各Page Objectに重複する
- 「ログイン→ダッシュボード確認」のようなテストが増えるたびに、ログイン処理が繰り返し実行され全体の実行時間が伸びる
- ページをまたぐ業務フロー(購入手続きなど)のテストコードが、複数のPage Objectを呼び出す長い羅列になる
この記事では、これらの課題を解決する4つの応用テクニックを順番に解説します。
この記事の位置づけ
この記事は、Page Object Model の基本(クラス設計・POMなし/ありの比較・ディレクトリ構成)を理解している方向けの応用編です。POMの基礎からおさらいしたい場合は、先に基礎記事を読むことをおすすめします。
① pytest FixtureでPage Objectを自動注入する方法
基本のPOMでは、各テストの中で LoginPage(page) のようにPage Objectを手動でインスタンス化します。テストが増えるとこの初期化コードがあらゆる場所に散らばり、テストの本質(何を検証したいか)が読みにくくなります。pytest Fixture でPage Objectの生成を肩代わりさせると、テストコードはPage Objectを受け取って使うだけになります。
❌ Fixture化前:毎回インスタンス化するコード
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage
def test_login_success(page):
# テストのたびにPage Objectのインスタンス化が必要
login_page = LoginPage(page)
login_page.navigate()
login_page.login("user@example.com", "secret123")
dashboard_page = DashboardPage(page)
assert dashboard_page.is_visible()
def test_login_failure(page):
# 同じインスタンス化コードがまた登場する
login_page = LoginPage(page)
login_page.navigate()
login_page.login("wrong@example.com", "wrongpass")
assert login_page.is_error_visible()✅ Fixture化後:conftest.py に注入処理をまとめる
# conftest.py
import pytest
from playwright.sync_api import Page
from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage
@pytest.fixture
def login_page(page: Page) -> LoginPage:
"""LoginPage を生成し、ログインページへの遷移まで済ませて渡す"""
lp = LoginPage(page)
lp.navigate()
return lp
@pytest.fixture
def dashboard_page(page: Page) -> DashboardPage:
return DashboardPage(page)# test_login.py
def test_login_success(login_page, dashboard_page):
# インスタンス化・遷移が済んだ状態から始められる
login_page.login("user@example.com", "secret123")
assert dashboard_page.is_visible()
def test_login_failure(login_page):
login_page.login("wrong@example.com", "wrongpass")
assert login_page.is_error_visible()② Component Object パターンで共通UIを切り出す
ヘッダー・サイドバー・通知モーダルなど、複数のページに共通して登場するUI要素をPage Objectごとに書いていると、同じロケーターや操作メソッドが何箇所にも重複します。こうした共通UIはComponent Objectとして独立させ、各Page Objectから合成(コンポジション)する設計にすると重複を防げます。
Component Object の定義
# components/header_component.py
from playwright.sync_api import Page
class HeaderComponent:
"""複数ページに共通して表示されるヘッダーのComponent Object"""
def __init__(self, page: Page):
self.page = page
self.logout_button = page.get_by_role("button", name="ログアウト")
self.notification_icon = page.get_by_test_id("notification-icon")
self.search_input = page.get_by_placeholder("検索")
def logout(self) -> None:
self.logout_button.click()
def open_notifications(self) -> None:
self.notification_icon.click()
def search(self, keyword: str) -> None:
self.search_input.fill(keyword)
self.search_input.press("Enter")Page Object から Component を合成する
# pages/dashboard_page.py
from playwright.sync_api import Page
from components.header_component import HeaderComponent
class DashboardPage:
"""ダッシュボードページのPage Object"""
def __init__(self, page: Page):
self.page = page
self.header = HeaderComponent(page) # ← Componentを合成する
self.summary_card = page.get_by_test_id("summary-card")
def is_visible(self) -> bool:
return self.summary_card.is_visible()# テストコードからは header 経由でアクセスできる
def test_logout_from_dashboard(dashboard_page):
dashboard_page.header.logout()
# ... ログアウト後の確認 ...| Component化しない | Component化する |
|---|---|
| ヘッダーのロケーターが10個のPage Objectに重複 | HeaderComponentの1箇所だけを修正すれば済む |
| ヘッダーの仕様変更で10ファイルを修正 | HeaderComponentの1ファイルだけを修正 |
③ storage_state で認証状態を再利用し高速化する
ログインが必要なテストが多いプロジェクトでは、すべてのテストで毎回ログイン処理を実行することが実行時間を伸ばす大きな要因になります。Playwrightの storage_state を使うと、一度ログインした状態(Cookie・localStorage)を保存し、以降のテストではログイン処理をスキップして再利用できます。
セッション単位で認証状態を1回だけ作成する
# conftest.py
import pytest
from playwright.sync_api import Browser, Page
@pytest.fixture(scope="session")
def authenticated_state(browser: Browser, base_url: str) -> dict:
"""テストスイート全体で1回だけログインし、認証状態を保存する"""
context = browser.new_context()
page = context.new_page()
page.goto(f"{base_url}/login")
page.get_by_label("メールアドレス").fill("test@example.com")
page.get_by_label("パスワード").fill("Password123")
page.get_by_role("button", name="ログイン").click()
page.wait_for_url("**/dashboard")
state = context.storage_state()
context.close()
return state
@pytest.fixture
def authenticated_page(browser: Browser, authenticated_state: dict) -> Page:
"""保存済みの認証状態から新しいページを作る(ログイン処理をスキップ)"""
context = browser.new_context(storage_state=authenticated_state)
page = context.new_page()
yield page
context.close()# test_dashboard.py
from pages.dashboard_page import DashboardPage
def test_dashboard_shows_summary(authenticated_page):
# ログイン処理なしで、認証済みの状態からテストを開始できる
dashboard_page = DashboardPage(authenticated_page)
authenticated_page.goto("/dashboard")
assert dashboard_page.is_visible()- テストがアカウント情報やパスワードなど、認証状態に影響するデータを書き換える場合、後続のテストに影響が及ぶことがあります
- 各テストが独立したデータ(別ユーザー・別レコード)を操作する設計にしておくと安全です
- 認証状態を書き換えるテスト(パスワード変更・ログアウトなど)は
authenticated_stateを使わず、個別にログインする設計にする方が安全な場合があります
④ Component / Page / Flow の3層で責務を分離する
「商品をカートに入れて購入を完了する」のような、複数ページをまたぐ業務フローをテストする場合、テストコードの中で複数のPage Objectを直接呼び出すと、テストの意図が読み取りにくくなります。こうしたケースでは、業務フローそのものをFlow Objectとしてカプセル化する設計が有効です。
Flow Object は「業務フローの再利用」が目的であり、Page Object を置き換えるものではありません。内部では既存のPage Objectを組み合わせて使うだけで、Page Object自体の役割(1画面の要素と操作)はそのまま残ります。
| 層 | 責務 | 例 |
|---|---|---|
| Component | 複数ページで共通するUI部品 | ヘッダー・モーダル・ページネーション |
| Page | 1画面の要素と操作 | カートページ・配送先入力ページ |
| Flow | 複数ページをまたぐ業務手順 | 購入手続き・会員登録フロー |
Flow Object の実装例
# flows/checkout_flow.py
from playwright.sync_api import Page
from pages.cart_page import CartPage
from pages.shipping_page import ShippingPage
from pages.payment_page import PaymentPage
class CheckoutFlow:
"""「購入手続き」という複数ページにまたがる業務フローをまとめる"""
def __init__(self, page: Page):
self.cart_page = CartPage(page)
self.shipping_page = ShippingPage(page)
self.payment_page = PaymentPage(page)
def complete_purchase(self, address: dict, card: dict) -> None:
"""カート確認から決済完了までを1メソッドで実行する"""
self.cart_page.proceed_to_checkout()
self.shipping_page.fill_address(address)
self.shipping_page.proceed()
self.payment_page.fill_card(card)
self.payment_page.submit()# test_checkout.py
def test_purchase_completes_successfully(checkout_flow):
# 複数ページの操作が1行で表現され、テストの意図が明確になる
checkout_flow.complete_purchase(
address={"postal_code": "150-0001", "city": "渋谷区"},
card={"number": "4242424242424242", "cvc": "123"},
)
assert checkout_flow.payment_page.is_confirmation_visible()応用編でよくある失敗パターン
① session scope の認証状態をテストが書き換えてしまう
authenticated_state を session scope にした状態で、あるテストがプロフィール情報やパスワードを変更すると、後続のテストが想定と異なる状態から始まってしまいます。認証状態を変更するテストは、専用のfunction scopeなfixtureに切り出しましょう。
② Component Object を作りすぎて管理コストが増える
「ボタン1つでもComponent化する」ように細かく分割しすぎると、逆にファイル数が増えて追いにくくなります。複数ページで実際に重複しているUIのみをComponent化するのが基本です。
③ Flow Object の中に assert を書いてしまう
Flow Objectは「操作をまとめる」役割に専念させ、判定(assert)はテストコード側に書くのが原則です。Flow内にassertが混ざると、何を検証しているテストなのかが読みにくくなります。
④ storage_state の有効期限切れに気づかない
セッションの有効期限が短いアプリケーションでは、保存した storage_state が途中で無効になり、原因不明の認証エラーとしてテストが落ちることがあります。テスト対象のセッション有効期限を把握した上で、必要に応じて再ログインのタイミングを調整しましょう。
📖 関連記事
FAQ
Q. 基本のPOM記事を読んでいなくても、この記事は理解できますか?
Page Object Modelの基本概念(ページごとにクラスを作る)を知っている前提で書いています。POMを初めて学ぶ場合は、先に基礎記事を読むことでスムーズに理解できます。
Q. Component Object と Page Object の境界線はどう決めればいいですか?
目安は「そのUIが2つ以上のページに登場するかどうか」です。1つのページにしか出てこない要素はそのPage Objectに直接書き、複数ページで共通するものだけをComponent Objectに切り出すと、過剰分割を避けられます。
Q. storage_state はどのくらいの頻度で作り直すべきですか?
プロジェクトのセッション有効期限に依存します。session scopeでテストスイート全体に1回だけ作る運用が基本ですが、有効期限が短いアプリケーションでは、テストの実行時間に応じて再ログインするタイミングを調整する必要があります。
Q. Flow Object は必ず作るべきですか?
必須ではありません。複数ページをまたぐ操作がテストの中で繰り返し登場するようになってから導入するのが実務的です。単一ページの操作だけで完結するテストが中心のプロジェクトでは、Flow Objectを作らずPage Objectだけで十分なケースも多いです。
Q. Component Object と Flow Object は両方必要ですか?
両方セットで導入する必要はありません。Component Objectは「共通UIの重複」が、Flow Objectは「複数ページをまたぐ操作の重複」がそれぞれ問題になったタイミングで個別に導入するものです。プロジェクトによっては、Component Objectだけ使ってFlow Objectは使わない、という組み合わせも十分にあり得ます。
Q. pytest-playwright プラグインを使わず、sync_playwright() を直接使う設計でもこの記事の内容は使えますか?
使えます。Fixture化・Component Object・Flow Objectの設計思想はpytest-playwrightの有無に関わらず適用できます。ただし page や browser といった組み込みFixtureはpytest-playwrightが提供するものなので、使わない場合はconftest.py側でご自身のFixtureとして用意する必要があります。
📋 まとめ
- pytest FixtureでPage Objectを自動注入すると、テストコードから初期化のボイラープレートが消える
- Component Objectで共通UIを切り出すと、複数ページに散らばるロケーターの重複を防げる
- storage_stateで認証済み状態を再利用すれば、ログイン処理の繰り返しによる実行時間の増加を抑えられる
- Flow Objectは複数ページをまたぐ業務手順をカプセル化し、テストの意図を読みやすくする
基本のPOMが「ページごとにクラスを分ける」設計だとすれば、この記事で紹介した応用テクニックは「テストスイート全体をどう構造化するか」という一段上の設計です。すべてを一度に導入する必要はありません。まずはFixture化から始めて、プロジェクトの規模に応じて少しずつ取り入れてみてください。
