Seleniumという「負債」を捨て、Playwrightでテストを自動化する:現場で生き残るための戦術
朝の4時。CIが落ちた通知で叩き起こされ、ログを追いかけて「要素が見つかりません」という無機質なエラーメッセージを眺める。そんなSeleniumの泥沼に浸かっている時間は、もう終わりだ。
正直に言おう。現代のフロントエンド開発において、Seleniumを選択するのは「自ら進んで苦行を選ぶ」のと同義だ。Cypressも悪くはないが、iframeや別タブの扱いで挙動不審になるたび、俺たちの生産性は削られていく。
本稿では、Playwrightへの移行で実行速度を3分から45秒に縮めた経験をもとに、現場レベルで「死なない」E2Eテスト環境を構築する知見を共有する。
1. なぜPlaywrightの「Locators」が最強なのか
Seleniumの脆弱なセレクタ指定に疲弊しているなら、PlaywrightのLocators APIを使うだけで世界が変わる。
PlaywrightのLocatorsは、単なるCSSセレクタのラッパーではない。Auto-waiting(要素が操作可能になるまで待つ)とStrict mode(複数の要素がマッチした場合に例外を投げる)が標準で組み込まれている。
# 良い例:厳密なセレクタ指定
# get_by_roleを使うことで、アクセシビリティに基づいた堅牢なテストが可能になる
submit_btn = page.get_by_role("button", name="ログイン")
submit_btn.click()
これだけで、「要素が描画される前にクリックして落ちる」というSelenium時代の古典的なバグが消滅する。
2. POM(Page Object Model)を「動的な管理」へ進化させる
単にクラスに分けるだけでは不十分だ。環境変数を利用して、ベースURLや認証情報を切り分けられるようにしろ。
import os
from playwright.sync_api import Page
class LoginPage:
def __init__(self, page: Page):
self.page = page
self.url = os.getenv("APP_BASE_URL", "https://staging.myapp.com")
self.username = page.get_by_label("ユーザー名")
self.password = page.get_by_label("パスワード")
def navigate(self):
self.page.goto(f"{self.url}/login")
def login(self, user, pwd):
self.username.fill(user)
self.password.fill(pwd)
self.page.get_by_role("button", name="送信").click()
こうすることで、ローカル、ステージング、CI環境での切り替えが環境変数一つで完結する。シークレット管理にはGitHub Secretsを活用し、コード内に認証情報をハードコーディングするような愚行は避けること。
3. CIの闇:非同期イベントと「無限ループ」の罠
以前、`page.on('console', ...)`でログを監視していた際、コールバック内で`page`の操作を行い、循環参照でメモリを食い潰してCIを爆破させたことがある。
Playwrightは強力な非同期イベント駆動モデルだが、リスナーの管理を怠ると即座に破綻する。
教訓:リスナーは使い終わったら即座に解除しろ。
def test_with_logging(page):
def log_handler(msg):
if "error" in msg.text:
print(f"Detected: {msg.text}")
page.on("console", log_handler)
try:
# テスト実行
page.goto("/")
finally:
# 確実に解除する設計にする
page.remove_listener("console", log_handler)
4. GitHub Actions:車輪の再発明はするな
自前でブラウザをインストールするDockerコンテナを作るのは、時間の無駄だ。公式イメージを使い、静的解析で「古い記法」を弾くのがプロの流儀だ。
`.eslintrc.json` や `flake8` を設定し、`page.waitForNavigation` のような非推奨コードをCIの時点でビルドエラーにしろ。AIが吐き出す「死んだコード」を本番環境に混入させないための防衛線だ。
# .github/workflows/e2e.yml
jobs:
test:
runs-on: ubuntu-latest
container: mcr.microsoft.com/playwright:v1.40.0-jammy
steps:
- uses: actions/checkout@v3
- name: Run tests
run: npx playwright test
env:
APP_BASE_URL: ${{ secrets.STAGING_URL }}
5. 最後に:メンテナンスし続けるための「型」
テストコードがゴミ屋敷化する最大の理由は「命名規則の欠如」と「ディレクトリ構成の混乱」にある。
推奨するディレクトリ構成:
tests/
├── e2e/ # ユーザーの行動ベースのテスト
├── fixtures/ # APIモックや認証状態の共通処理
├── pages/ # POMの定義
└── conftest.py # Pytestのフィクスチャ定義
テスト設計のコツは、「テストデータとロジックの完全分離」だ。APIモックには `page.route()` を使い、バックエンドの不安定さにテストが左右されない環境を構築しろ。
Playwrightは魔法の杖ではない。しかし、お前が「テストの設計」に集中するための、現時点で最も鋭利なナイフだ。道具を使いこなすか、道具に振り回されて深夜のデバッグに明け暮れるか。すべては、今日お前が書くコードの「設計力」にかかっている。