コンテンツにスキップ

Python開発: Playwrightによる効率的なWebテスト

はじめに:Webテスト自動化におけるPlaywrightの優位性

Webアプリケーション開発において、ブラウザ自動化はテスト効率化の要となります。特に動的UIを多用する現代のWebアプリケーションでは、従来のツールでは安定性に課題がありました。この記事では、Microsoftが開発したオープンソースフレームワーク「Playwright」を活用したPythonでのブラウザ操作自動化について詳しく解説します。

Playwrightは、Chromium、WebKit、Firefoxの主要レンダリングエンジンをサポートし、Windows、Linux、macOSでのクロスプラットフォーム動作を実現します。TypeScript、JavaScript、Python、.NET、Javaといった複数のプログラミング言語に対応し、モバイルWebのテストもネイティブエミュレーションを通じてサポートされています。

従来のSeleniumなどのツールと比較して、Playwrightは自動待機(Auto-wait)機能や信頼性の高いLocator APIを提供し、テストの不安定さを大幅に削減します。これにより、開発者は明示的な待機処理を記述する必要性が減り、より効率的なテスト開発が可能になります。

本記事では、Playwrightの基本から応用まで、具体的なサンプルコードを交えながら詳細に解説します。

1. Playwrightのインストールと環境構築

前提条件

Playwrightを利用するには、以下の要件を満たす必要があります:

  • Python: バージョン3.9以上
  • 対応OS:
    • Windows 11 / Windows Server 2022 / WSL
    • macOS 15 以降
    • Linux (Debian 12, Ubuntu 22.04/24.04など、x86-64/arm64)

インストール手順

Playwrightのインストールは主に2つのステップで行います:

ステップ1: Playwright Pythonライブラリのインストール

# pipを使用 (推奨)
pip install playwright
# またはCondaを使用
# conda config --add channels conda-forge
# conda config --add channels microsoft
# conda install playwright

ステップ2: ブラウザバイナリのインストール

playwright install

この2段階のインストールプロセスは重要です。pip install playwrightはPythonライブラリ本体をインストールし、playwright installは実際にブラウザを操作するための実行ファイル群をダウンロードします。これらのブラウザバイナリはOS固有のキャッシュディレクトリに保存されます。 特定のブラウザのみをインストールしたい場合は、playwright install chromiumのように指定できます。

プロジェクト設定

仮想環境を使って依存関係を分離することを強く推奨します:

# 仮想環境の作成
python -m venv venv
# 仮想環境のアクティベート
# Windows:.\venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
# Playwrightのインストール
pip install playwright
playwright install

E2Eテストを作成する場合は、公式のpytest-playwrightプラグインの使用をお勧めします:

pip install pytest-playwright
playwright install

2. Playwrightの基本的な操作

同期APIと非同期API

Playwrightは同期(Sync)APIと非同期(Async)APIの両方を提供しています:

# 同期APIの例
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
   browser = p.chromium.launch()
   page = browser.new_page()
   page.goto("https://playwright.dev/python")
   print(f"ページタイトル (Sync): {page.title()}")
   browser.close()

# 非同期APIの例
import asyncio
from playwright.async_api import async_playwright

async def main():
   async with async_playwright() as p:
       browser = await p.chromium.launch()
       page = await browser.new_page()
       await page.goto("https://playwright.dev/python")
       print(f"ページタイトル (Async): {await page.title()}")
       await browser.close()

asyncio.run(main())

プロジェクトが既にasyncioを使用している場合は非同期APIを、そうでない場合は同期APIを使用するのが一般的です。

ブラウザの起動

# Firefoxをヘッダーモード、500msの遅延付きで起動する例 (同期API)
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
   browser = p.firefox.launch(headless=False, slow_mo=500)
   page = browser.new_page()
   page.goto("https://example.com")
   #... 要素操作など...
   print(f"現在のURL: {page.url}")
   browser.close()

デフォルトではブラウザはヘッドレスモード(headless=True)で起動します。デバッグや視覚的な確認が必要な場合は、headless=Falseを指定してヘッダーモードで起動しましょう。

ブラウザコンテキストとページ

Playwrightでは、ブラウザインスタンス内に複数の独立したセッション環境(ブラウザコンテキスト)を作成できます:

# コンテキストとページの作成例 (同期API)
browser = p.chromium.launch()
context = browser.new_context(
   viewport={'width': 1280, 'height': 720},
   locale='ja-JP' # ロケールを日本語に設定
)
page = context.new_page()
print(f"ページを作成しました。現在のコンテキスト内のページ数: {len(context.pages)}")

page.goto("https://www.google.com")
print(f"ページのタイトル: {page.title()}")
context.close() # コンテキストを閉じると、その中の全ページも閉じられる
browser.close()

各ブラウザコンテキストは完全に分離されており、Cookieやストレージなどを共有しません。

ページのナビゲーション

ページを特定のURLに移動させる主な方法はpage.goto(url)です:

# ナビゲーションの例 (同期API)
page.goto("https://github.com")
print(f"現在のURL: {page.url}")
# リンクをクリックして暗黙的にナビゲーション
page.locator('a[href="/explore"]').click() # exploreページへ移動
print(f"移動後のURL: {page.url}")

Web要素の特定

Playwrightで要素を操作するには、まず対象の要素を特定する必要があります。この中心的な概念がLocator(ロケータ)です:

# 推奨されるLocator
login_button = page.get_by_role("button", name="ログイン")
username_input = page.get_by_label("ユーザー名")
product_title = page.get_by_text("素晴らしい製品", exact=True) # 完全一致
error_message = page.get_by_test_id("error-message")
image_logo = page.get_by_alt_text("会社のロゴ")

# CSSセレクタ
submit_button_css = page.locator("button[type='submit']")
# XPathセレクタ
specific_div_xpath = page.locator("//div[@class='container']/p[1]")

# チェイン
cart_item_price = page.locator("li.cart-item").locator(".price")
# and_ を使ったチェイン
subscribe_button = page.get_by_role("button").and_(page.get_by_title("購読"))

Playwrightの推奨されるLocator戦略(ユーザー中心)は以下の通りです:

  • page.get_by_role(role, **options): ARIAロール、アクセシブルネームに基づいて要素を特定
  • page.get_by_text(text, exact=False): 指定されたテキストを含む要素を見つける
  • page.get_by_label(text): ラベルテキストに基づいてフォームコントロールを特定
  • page.get_by_placeholder(text): プレースホルダーテキストで入力フィールドを特定
  • page.get_by_alt_text(text): alt属性のテキストで要素(通常は画像)を特定
  • page.get_by_title(text): title属性に基づいて要素を特定
  • page.get_by_test_id(test_id): data-testid属性に基づいて要素を特定

要素の操作

要素を特定したら、Locatorオブジェクトに対して直接メソッドを呼び出して操作を行います:

# pageが初期化されていると仮定
page.goto("your_web_app_url")

# ラベルで特定した入力フィールドに入力
page.get_by_label("メールアドレス").fill("test@example.com")
page.get_by_label("パスワード").fill("s3cr3tP@ssw0rd")
# ロールと名前で特定したボタンをクリック
page.get_by_role("button", name="ログイン").click()

# チェックボックスをチェック
page.get_by_label("利用規約に同意する").check()
# セレクトボックスで値 'monthly' を選択
page.get_by_label("更新頻度を選択").select_option("monthly")
# リンクにホバー
page.get_by_role("link", name="プロフィール").hover()
# ボタンでEnterキーを押下
page.get_by_role("button", name="検索").press("Enter")

3. 効率的で信頼性の高いスクリプト作成

自動待機機能の威力

Playwrightの最大の強みの一つが自動待機(Auto-wait)機能です。この機能により、要素が操作可能になるまで自動的に待機するため、明示的な待機処理が不要になります:

# Locatorが動的コンテンツに対応する概念実証コード
# (同期API内)

# 動的に更新されるリストを想定
list_items = page.locator("ul#dynamic-list > li")

# 初期状態のリスト項目数を取得 (DOMクエリ実行)
initial_count = list_items.count()
print(f"初期の項目数: {initial_count}")

# JavaScriptによってリスト項目が追加されるアクションをシミュレート
page.get_by_role("button", name="項目を追加").click()

# 同じLocatorを使って再度項目数を取得 (DOMクエリ再実行)
# Locatorは最新のDOM状態を反映する
count_after_add = list_items.count()
print(f"追加後の項目数: {count_after_add}")

# アサーションもLocatorを使用する
from playwright.sync_api import expect
expect(list_items).to_have_count(initial_count + 1)

Playwrightは要素に対して「操作可能性チェック(Actionability Checks)」を実行し、以下を確認します:

  • Attached: 要素がDOMにアタッチされているか
  • Visible: 要素が非表示でないか
  • Stable: 要素がアニメーション中でなく、位置やサイズが安定しているか
  • Enabled: 要素が無効化されていないか
  • Editable: (入力操作の場合)要素が編集可能か
  • Receives Events: 要素が他の要素に覆われておらず、イベントを受け取れる状態か

Codegenによる開発の高速化

Playwright Codegenは、テストスクリプトの初期作成を支援する強力なツールです:

# example.com を開き、Pythonコードを my_test_script.py に出力
playwright codegen --target python -o my_test_script.py https://example.com

コマンドを実行すると、指定されたURLを開いたブラウザウィンドウと、Playwright Inspectorウィンドウが起動します。ユーザーがブラウザ上で行った操作が記録され、対応するPythonコードがリアルタイムで生成されます。

Playwright InspectorとTrace Viewerによる効果的なデバッグ

Playwright Inspector

# Inspectorで実行
PWDEBUG=1 pytest -s your_test_file.py

Playwright Inspectorを使用すると、ステップ実行、実行ログの表示、Locatorのライブ選択・編集・検証などが可能です。

Trace Viewer

# トレースをプログラム的に有効化する例 (同期API)
browser = p.chromium.launch()
context = browser.new_context()
# トレース記録を開始 (スクリーンショット、DOMスナップショット、ソースを含む)
context.tracing.start(screenshots=True, snapshots=True, sources=True)

test_failed = False # テスト結果を保持する変数
try:
   page = context.new_page()
   # --- テストロジック ---
   page.goto("https://example.com")
   page.locator("h1").click()
   # アサーションなどで失敗を検出したら test_failed = True にする
   expect(page).to_have_title("NonExistentTitle") # わざと失敗させる例
except Exception:
   test_failed = True
   raise # エラーを再送出
finally:
   # テストが失敗した場合のみ trace.zip に保存
   if test_failed:
       print("テスト失敗: トレースを trace.zip に保存します。")
       context.tracing.stop(path="trace.zip")
   else:
       print("テスト成功: トレースを破棄します。")
       context.tracing.stop() # 保存せずに停止
   context.close()
   browser.close()

# トレースを表示
# playwright show-trace trace.zip

Trace Viewerを使用すると、テスト実行のトレース(操作、DOMスナップショット、ネットワークリクエスト、コンソールログなど)を記録し、後からGUIで確認できます。

4. Page Object Model (POM)によるテストの構造化

Page Object Model(POM)は、テストコードの保守性、再利用性、可読性を向上させるための効果的なデザインパターンです:

# models/login_page.py
from playwright.sync_api import Page, Locator, expect
from .base_page import BasePage

class LoginPage(BasePage):
   def __init__(self, page: Page):
       super().__init__(page)
       self.page = page
       self.username_input: Locator = page.get_by_label("ユーザー名")
       self.password_input: Locator = page.get_by_label("パスワード")
       self.login_button: Locator = page.get_by_role("button", name="ログイン")
       self.error_message: Locator = page.locator(".error-message")

   def navigate(self, url: str):
       """指定されたURLに移動します"""
       self.page.goto(url)
       expect(self).to_have_title("ログイン")

   def login(self, username: str, password: str):
       """ユーザー名とパスワードを入力してログインボタンをクリックします"""
       self.username_input.fill(username)
       self.password_input.fill(password)
       self.login_button.click()

   def check_error_message(self, expected_text: str):
       """エラーメッセージが表示され、期待されるテキストを含むことを確認します"""
       expect(self.error_message).to_be_visible()
       expect(self.error_message).to_have_text(expected_text)
# models/base_page.py
from playwright.sync_api import Page, expect
import re

class BasePage:
   def __init__(self, page: Page):
       self.page = page
       # 例: 全ページ共通のヘッダー要素など
       # self.header_logo = page.locator("#header-logo")

   def get_title(self) -> str:
       """現在のページのタイトルを取得します"""
       return self.page.title()

   def expect_to_have_title(self, title_pattern: str | re.Pattern):
        """ページタイトルが指定されたパターンに一致することを期待します"""
        expect(self.page).to_have_title(title_pattern)
# tests/test_login.py
import pytest
from playwright.sync_api import Page, expect
from models.login_page import LoginPage
from models.dashboard_page import DashboardPage

@pytest.fixture(scope="function")
def login_page(page: Page) -> LoginPage:
   return LoginPage(page)

@pytest.fixture(scope="function")
def dashboard_page(page: Page) -> DashboardPage:
   return DashboardPage(page)

LOGIN_URL = "https://your-app.com/login"

def test_successful_login(login_page: LoginPage, dashboard_page: DashboardPage):
   """有効な認証情報でログインが成功することを確認します"""
   login_page.navigate(LOGIN_URL)
   login_page.login("valid_user", "valid_pass")

   # ログイン成功のアサーション
   dashboard_page.expect_welcome_message_to_be_visible("valid_user")

def test_invalid_login(login_page: LoginPage):
   """無効な認証情報でエラーメッセージが表示されることを確認します"""
   login_page.navigate(LOGIN_URL)
   login_page.login("invalid_user", "wrong_pass")

   # LoginPageのメソッドでエラーメッセージを検証
   login_page.check_error_message("無効な認証情報です")
   # URLが変わっていないことなども検証可能
   expect(login_page.page).to_have_url(LOGIN_URL)

5. Playwrightの応用機能

ネットワークリクエストの傍受とモック

from playwright.sync_api import Page, Route, expect
import re

def test_mock_api_users(page: Page):
   # モックするデータ
   mock_users_data = [
       {"id": 1, "name": "Alice", "email": "alice@example.com"},
       {"id": 2, "name": "Bob", "email": "bob@example.com"}
   ]

   # ルートハンドラ関数
   def handle_user_api(route: Route):
       print(f"傍受したリクエスト: {route.request.method} {route.request.url}")
       # リクエストをモックJSONデータで完了させる
       route.fulfill(
           status=200,
           content_type="application/json",
           json=mock_users_data
       )

   # URLパターンに一致するリクエストを傍受
   page.route(re.compile(r"/api/v1/users$"), handle_user_api)

   # ページに移動
   page.goto("https://your-app.com/users")

   # モックデータが表示されていることをアサート
   expect(page.get_by_text("Alice")).to_be_visible()
   expect(page.get_by_text("Bob")).to_be_visible()
   expect(page.locator(".user-list-item")).to_have_count(2)

   # 例: 画像リクエストをブロック
   page.route("**/*.{png,jpg,jpeg}", lambda route: route.abort())
   page.reload()
   # ... 画像が表示されていないことのアサーション...

複数ページ、タブ、ポップアップの処理

from playwright.sync_api import Page, expect
import re

def test_handle_new_tab_opened_by_link(page: Page):
   page.goto("https://your-app-with-target-blank-link.com")

   # expect_page コンテキストマネージャを使用
   print("新しいページが開かれるのを待機します...")
   with page.context.expect_page() as new_page_info:
       page.get_by_role("link", name="ドキュメントを開く").click() # target="_blank" のリンクをクリック

   # コンテキストマネージャ終了後、新しいページの Page オブジェクトを取得
   new_page = new_page_info.value
   print(f"新しいページを取得しました: {new_page.url}")

   # 新しいページが完全に読み込まれるのを待つ (推奨)
   new_page.wait_for_load_state("domcontentloaded")
   print(f"新しいページのタイトル: {new_page.title()}")
   expect(new_page).to_have_title(re.compile("ドキュメント"))

   # 新しいページ上の要素を操作・検証
   expect(new_page.get_by_role("heading", name="はじめに")).to_be_visible()

   # 元のページ上の要素も引き続き操作・検証可能
   expect(page.get_by_role("heading", name="メインアプリケーション")).to_be_visible()
   print(f"元のページのタイトル: {page.title()}")

   # 必要であれば新しいページを閉じる
   new_page.close()
   print("新しいページを閉じました。")
   # context.pages で現在のページリストを確認
   print(f"現在のページ数: {len(page.context.pages)}")

スクリーンショットと動画記録

from playwright.sync_api import sync_playwright, expect
import os

# 保存先ディレクトリを作成
os.makedirs("videos_output", exist_ok=True)
os.makedirs("screenshots_output", exist_ok=True)

with sync_playwright() as p:
   browser = p.chromium.launch()
   # このコンテキストの操作を動画記録
   context = browser.new_context(
       record_video_dir="videos_output/",
       record_video_size={"width": 1280, "height": 720},
       viewport={"width": 1280, "height": 720} # ビューポートを動画サイズに合わせる
   )
   page = context.new_page()

   video_path = None # 動画パスを保持する変数
   try:
       page.goto("https://playwright.dev/")

       # 特定要素のスクリーンショット
       page.locator(".hero__title").screenshot(path="screenshots_output/hero_title.png")

       # 要素をマスクしてスクリーンショット
       page.screenshot(
           path="screenshots_output/masked_screenshot.png",
           mask=[page.locator(".hero__title")]
       )

       page.get_by_role("link", name="Get started").click()
       expect(page).to_have_url(re.compile("docs/intro"))

       # フルページのスクリーンショット
       page.screenshot(path="screenshots_output/get_started_page_full.png", full_page=True)

   finally:
       # ★重要: コンテキストを閉じて動画を保存
       context.close()
       # コンテキストクローズ後にビデオパスを取得可能
       if page.video:
            video_path = page.video.path()
            print(f"動画が保存されました: {video_path}")
       else:
            print("動画は記録されませんでした。")
       browser.close()

まとめ:Playwrightによる効率的な自動化の構築

本記事を通じて、PlaywrightがPythonを用いたブラウザ自動化において、いかに効率性と信頼性をもたらすかを紹介しました。Playwrightの主な強みは以下の通りです:

  • 信頼性: 自動待機機能と堅牢なLocator APIにより、動的なUIに対するテストの不安定さを大幅に削減
  • 速度と効率: 軽量なブラウザコンテキスト、並列実行のサポート、効率的な通信プロトコルによる高速なテスト実行
  • クロスブラウザ/プラットフォーム: 単一のAPIで主要なブラウザエンジンとOSをサポート
  • 包括的な機能: ネットワークの傍受・モック、複数ページの操作、ファイルアップロード、スクリーンショット/動画記録など、幅広い自動化ニーズに対応
  • 優れた開発者体験: Codegenによる迅速なコード生成、Playwright Inspectorによるデバッグ、Trace Viewerによる詳細な実行分析

ベストプラクティス

Playwrightを最大限に活用するためのベストプラクティスをまとめます:

  1. Locator戦略: get_by_role, get_by_text, get_by_label, get_by_test_id といったユーザー中心のLocatorを優先的に使用する
  2. Locator APIの活用: 常に page.locator または page.get_by_* を使用し、非推奨の ElementHandle の使用は避ける
  3. Page Object Model (POM): テストスイートが一定規模以上になる場合は、POMパターンを導入する
  4. Codegenの活用: テストの初期作成やLocatorの調査にはCodegenを積極的に利用する
  5. デバッグツールの活用: 開発中はPlaywright Inspectorを、CI環境での失敗分析にはTrace Viewerを活用する
  6. ネットワークモック: テストの安定性向上や特定シナリオのテストのために、ネットワークリクエストの傍受・モック機能を戦略的に利用する
  7. 視覚的証拠: CI環境では、テスト失敗時にスクリーンショットや動画が自動的に保存されるように設定する
  8. テストランナー: pytest-playwright プラグインを利用して、テストの実行、フィクスチャ管理、並列実行などを効率化する

Playwrightは、現代のWeb開発におけるブラウザ自動化とE2Eテストの要求に応える強力なフレームワークです。本記事で解説した機能とプラクティスを活用することで、より効率的かつ信頼性の高い自動化を実現できるでしょう。

(c) Lions Data, LLC.