最新自動テストツール『Playwright for Python』さわってみた

JavaScript向けのE2E自動テストツールであった「Playwright」が、
**Python向けに「Playwright for Python」としてプレビューリリースされた**ということで早速さわってみました。

[Announcing Playwright for Python: Reliable end-to-end testing for the web](https://devblogs.microsoft.com/python/announcing-playwright-for-python-reliable-end-to-end-testing-for-the-web/)

## Playwrightとは

PlaywrightはChromium, Firefox, Webkitを単一のAPIで自動化するライブラリです。
これを利用することで高速で信頼性の高いクロスブラウザテストが実施できるようになります。

### Playwrightの利点

[公式ドキュメント](https://playwright.dev/#version=v1.4.2&path=docs%2Fwhy-playwright.md&q=)では以下の4点が述べられています。以下拙訳です。

#### 1. 全てのブラウザをサポート

- Chromium, Firefox, Webkit(Safari)をサポートしています。
- Webkitのクロスプラットフォームテストが可能です。(Windows, Mac, Linux)
- モバイル向けのブラウザテストができます。
- ヘッドレスモードでもヘッドフルモードでも実行できます。

#### 2. 高速で信頼性のあるテスト実行

- APIが自動でwaitします。
- タイムアウトのない自動化ができます。
- [ブラウザコンテキスト](https://playwright.dev/#version=v1.4.2&path=docs%2Fcore-concepts.md&q=browser-contexts)を用いた並列実行が可能です。
- 変更に強い要素セレクタが使えます。

#### 3. パワフルな自動化を実現

- [複数のドメイン、ページ、フレームに対応しています。](https://playwright.dev/#version=v1.4.2&path=docs%2Fmulti-pages.md&q=)
- パワフルなネットワーク制御ができます。
- モダンなWebの機能が使えます。(Shadow DOM, ジオロケーション, パーミッション, etc...)
- 全てのシナリオをカバーする機能を有しています。(ファイルDL/ULからダークモードまで)

#### 4. ワークフローとの統合

- ワンラインでインストールできます。
- TypeScriptをサポートしています。
- デバッグツールと統合できます。
- PythonとC#バインディングが用意されています。
- CIにテストをデプロイできます。(公式Dockerイメージ、GitHub Actionsの提供)

## ハンズオン

それではここからは実際にテストケースを書いていこうと思います。
テスト対象には[HOTEL PLANISPHEREの宿泊予約(お得な特典付きプラン)ページ](https://hotel.testplanisphere.dev/ja/reserve.html?plan-id=0)を使用させていただきました。
テストケースは「テスト対象のページにアクセスし、各パラメータを設定したのち、予約確認ページに遷移して、そこに表示される金額が15000円だったらPass」、という正常系のテストとしました。

### インストール

[公式GitHubの記載に沿ってインストールを実施します。](https://github.com/microsoft/playwright-python)
また今回は単体テストフレームワークにpytestを利用するため、[更にpytestプラグインをインストールします。](https://github.com/microsoft/playwright-pytest)

```[cmd]
pip install playwright
python -m playwright install
pip install pytest-playwright
```

なお今回は実施しませんが、非同期処理を実施したい場合は別途`pip install asyncio`してください。

### まずSeleniumでテストを書いてみる

Playwrightに触る前に、参考としてSelenium Webdriverで今回実現したいテストケースを書いてみました。
import文の多さ、動的コンテンツに対する待機処理、ブラウザごとのWebdriverの管理あたりでなかなか苦戦しますね…。

```selenium.py
import time

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select, WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager


class TestPlanisphere:
    def test_reserve_otoku(self):
        driver = webdriver.Chrome(ChromeDriverManager().install())
        wait = WebDriverWait(driver, 10)

        driver.get("https://hotel.testplanisphere.dev/ja/reserve.html?plan-id=0")
        # NOTE: wait.until(EC.presence_of_all_elements_located) <- 上手く動作せず
        time.sleep(1)  # HACK

        date = driver.find_element_by_id("date")  # 宿泊日
        date.clear()
        date.send_keys("2020/12/24")
        date.send_keys(Keys.TAB)

        term = driver.find_element_by_id("term")  # 宿泊数
        term.clear()
        term.send_keys("2")

        head_count = driver.find_element_by_id("head-count")  # 人数
        head_count.clear()
        head_count.send_keys("1")

        sightseeing = driver.find_element_by_id("sightseeing")  # 「お得な観光プラン」選択
        if not sightseeing.is_selected():
            sightseeing.click()

        driver.find_element_by_id("username").send_keys("ベリ坊")  # 名前

        Select(driver.find_element_by_id("contact")).select_by_value("tel")  # 「電話連絡を希望」

        driver.find_element_by_id("tel").send_keys("80120828828")  # 電話番号

        driver.find_element_by_id("submit-button").click()  # 予約確認ページへ遷移
        wait.until(EC.presence_of_all_elements_located)

        driver.save_screenshot("selenium.png")  # スクリーンショット撮影

        assert "15,000" in driver.find_element_by_id("total-bill").text

        driver.quit()
```

### Playwright for Pythonでテストを書いてみる

次に同じテストをPlaywright for Pythonで書いてみました。
今回使うメソッドは公式ドキュメントの以下のページにまとまっています。JavaScriptのみの記載ですが、Pythonでもほとんどまるごと流用できます。

- [Element Selectors](https://playwright.dev/#version=v1.4.2&path=docs%2Fselectors.md&q=)
- [Input](https://playwright.dev/#version=v1.4.2&path=docs%2Finput.md&q=)
- [Assertions](https://playwright.dev/#version=v1.4.2&path=docs%2Fassertions.md&q=)

```play.py
from playwright.sync_api import Page


class TestPlanisphere:
    def test_reserve_otoku(self, page: Page):
        page.goto("https://hotel.testplanisphere.dev/ja/reserve.html?plan-id=0")
        page.waitForLoadState("networkidle")

        page.fill("#date", "2020/12/24")  # 宿泊日
        page.press("#date", "Tab")

        page.fill("#term", "2")  # 宿泊数

        page.fill("#head-count", "1")  # 人数

        page.check("#sightseeing")  # プラン選択

        page.fill("#username", "ベリ坊")  # 名前

        page.selectOption("#contact", "tel")  # 「電話連絡を希望」
        page.fill("#tel", "80120828828")  # 電話番号

        page.click("#submit-button")  # 予約確認ページへ遷移
        page.waitForLoadState("networkidle")

        page.screenshot(path="playwright.png")  # スクリーンショット撮影

        assert "15,000" in page.innerText("#total-bill")

        page.close()
```

Seleniumと比較すると、

- モジュールの構成がシンプルで、import文がほぼ不要
  - Page型をインポートした上でpylanceと組み合わせると、メソッドの検索をVSCode上で完結できて非常に便利
- クロスブラウザ対応(起動するブラウザはpytestの実行時に指定可能)
- 各ブラウザはパッケージにビルトインされており、webdriverのバージョン管理のようなことも不要
- 動的コンテンツに対する待機処理が優秀

などなど、Seleniumで辛かったところを見事に解決しているのがスクリプトを見ただけでもわかるのではないでしょうか。

### pytestでPlaywrightのテストを実行する

あとはpytestで上記のplay.pyを実行するだけです。実行時のオプションでヘッドレス/ヘッドフル、実行ブラウザ指定ができます。
[ファイル`pytest.ini`に起動オプションを保存しておくこともできます。](https://github.com/microsoft/playwright-pytest#usage)

```cmd
# テスト実行 (ヘッドレスChromiumがデフォルト)
pytest play.py

# ヘッドフルモードでテスト実行
pytest --headful play.py

# ブラウザを指定してテスト実行  (chromium, firefox, webkit)
pytest --browser firefox play.py

# 複数のブラウザでテスト実行
pytest --browser chromium --browser webkit play.py
```

## 所感

私は自動化ツールの進化には2つの方向性があると思っていて、
1つはレポーティング機能やCI機能などを統合した多機能なテストプラットフォームとして進化していく方向性と、もう1つは軽量高速でプログラマブルなツールとして進化していく方向性があると思っています。
Playwrightは後者の有力な選択肢になるのかなと感じました。

Microsoftという巨人の後ろ盾(更に原点はGoogleのPuppeteerですし…)があるのがOSSとして非常にパワフルで、今後の機能拡張も見込めますので、プレビューリリースの今のうちから触りだすのがよいかと思います。

個人的にも正直Seleniumよりずっと楽で学習コストも低いと感じたので、使えるシーンではSeleniumより優先して採用していこうと思います！