【Pytest × Playwright】テスト関数に使われる引数pageは何をしているのか

Pytest-Playwrightを使用して、E2Eテスト自動化を行うとき、テスト関数の引数にpageを用います。[^1]

以下のサンプルスクリプトは[codegen](https://playwright.dev/python/docs/codegen-intro)を使って、[HOTEL PLANISPHERE](https://hotel.testplanisphere.dev/ja/reserve.html?plan-id=0)の自動テストスクリプトを作成したものです。

```sample.py
from playwright.sync_api import Page, expect


def test_example(page: Page) -> None:
    page.goto("https://hotel.testplanisphere.dev/ja/reserve.html?plan-id=0")
    page.get_by_label("宿泊日 必須").click()
    page.get_by_label("宿泊日 必須").fill("2024/07/01")
    page.get_by_label("宿泊日 必須").press("Tab")
    page.get_by_label("宿泊数 必須").fill("3")
    page.get_by_label("宿泊数 必須").press("Tab")
    page.get_by_label("人数 必須").fill("2")
    page.get_by_text("お得な観光プラン").click()
    page.get_by_label("氏名 必須").click()
    page.get_by_label("氏名 必須").fill("TEST")
    page.get_by_label("確認のご連絡 必須").select_option("no")
    page.locator("[data-test=\"submit-button\"]").click()
    expect(page.get_by_role("heading", name="合計 44,000円（税込み）")).to_be_visible()

```

[Playwright公式ページのサンプルスクリプト](https://playwright.dev/python/docs/writing-tests#first-test)にも、pageは使われていますが、一体このpageは何をしているのでしょうか。
触り始めた当初は「playwright使うときはpageが必要」というくらいの感覚でした。
最近気になったので、調べてみました。

## 引数pageはpytest-playwrightが提供しているfixtureメソッド

このpageはPlaywrightの**プラグイン**である[pytest-playwright](https://playwright.dev/python/docs/test-runners)が提供している**fixture**メソッドでした。

### fixtureメソッドとは

fixtureメソッドとは、テストの前後処理をまとめている関数です[^1]。
テスト関数の引数でfixtureメソッド名を指定するとテスト関数実行前と実行後に指定されたfixtureメソッドが実行されます。

```test_with_pytest_fixture.py
import pytest

@pytest.fixture()
def example_fixture():
    # 前処理
    print("テスト実施前")

    # ここでテストを実行
    yield "テスト実施"

    # 後処理
    print("テスト完了")

def test_foo(example_fixture):
    print(example_fixture)
    assert True
```

👆のスクリプトを実行（`pytest -s`）すると以下になります。

```cmd
テスト実施前
テスト実施
テスト完了
```

### pageメソッドはテスト用のブラウザページを生成している

テスト関数の引数にpageを使うと、以下のメソッドが実行されます。

|メソッド|内容|
|:--:|:--|
|new_context|[pytestコマンド実行時に指定された引数](https://playwright.dev/python/docs/test-runners#cli-arguments)に基づいて、起動するブラウザの種類等を設定します。|
|context|テスト用の新しい[ブラウザコンテキスト](https://playwright.dev/python/docs/api/class-browsercontext)を生成します。|
|page|テスト用の新しい[ブラウザページ](https://playwright.dev/python/docs/api/class-page)を生成します。|

```pytest_playwright.py

@pytest.fixture
def new_context(
    browser: Browser,
    browser_context_args: Dict,
    _artifacts_recorder: "ArtifactsRecorder",
    request: pytest.FixtureRequest,
) -> Generator[CreateContextCallback, None, None]:
    browser_context_args = browser_context_args.copy()
    context_args_marker = next(request.node.iter_markers("browser_context_args"), None)
    additional_context_args = context_args_marker.kwargs if context_args_marker else {}
    browser_context_args.update(additional_context_args)
    contexts: List[BrowserContext] = []

    def _new_context(**kwargs: Any) -> BrowserContext:
        context = browser.new_context(**browser_context_args, **kwargs)
        original_close = context.close

        def _close_wrapper(*args: Any, **kwargs: Any) -> None:
            contexts.remove(context)
            _artifacts_recorder.on_will_close_browser_context(context)
            original_close(*args, **kwargs)

        context.close = _close_wrapper
        contexts.append(context)
        _artifacts_recorder.on_did_create_browser_context(context)
        return context

    yield cast(CreateContextCallback, _new_context)
    for context in contexts.copy():
        context.close()


@pytest.fixture
def context(new_context: CreateContextCallback) -> BrowserContext:
    return new_context()


@pytest.fixture
def page(context: BrowserContext) -> Page:
    return context.new_page()
```

[^1]:https://docs.pytest.org/en/latest/explanation/fixtures.html

pytest-playwrightのpageメソッドを使わない場合は以下のようになります。

```pytest_without_fixture_method.py
from playwright.sync_api import sync_playwright, Playwright

def test_reserve() -> None:
    with sync_playwright() as playwright:
        firefox = playwright.firefox
        browser = firefox.launch()
        page = browser.new_page()
        page.goto("https://hotel.testplanisphere.dev/ja/reserve.html?plan-id=0")
        page.get_by_label("宿泊日 必須").click()
        page.get_by_label("宿泊日 必須").fill("2024/07/01")
        page.get_by_label("宿泊日 必須").press("Tab")
        page.get_by_label("宿泊数 必須").fill("3")
        page.get_by_label("宿泊数 必須").press("Tab")
        page.get_by_label("人数 必須").fill("2")
        page.get_by_text("お得な観光プラン").click()
        page.get_by_label("氏名 必須").click()
        page.get_by_label("氏名 必須").fill("TEST")
        page.get_by_label("確認のご連絡 必須").select_option("no")
        page.locator("[data-test=\"submit-button\"]").click()
        expect(page.get_by_role("heading", name="合計 44,000円（税込み）")).to_be_visible()
        browser.close()
```

運用することを踏まえると素直にpytest-playwright提供のfixtureメソッドを使った方がよさそうです。[^2]

[^2]:pytest_without_fixture_method.pyではテスト失敗の場合、ブラウザを閉じません。そのため、fixtureメソッドやteardown_method等を活用する必要があります。

pytest-playwrightが提供しているfixtureメソッド一覧は以下のページに記載されています。

https://playwright.dev/python/docs/test-runners#fixtures

## 参考資料

- [Pytest Plugin Reference](https://playwright.dev/python/docs/test-runners)
- [github pytest-playwright](https://github.com/microsoft/playwright-pytest/blob/main/pytest_playwright/pytest_playwright.py)