ChatGPT（GPT-4）にE2E自動テストを書かせてみた

2023/3/15は完全にGPT-4の話題でもちきりでした。

私も触ってみたところ「これはすごい」と思ったので即課金して色々試していまして、
本業の一部であるE2Eの自動テストを書かせたらどうなるんだろう、を試してみました。

## 条件など

- テスト対象は[HOTEL PLANISPHERE - テスト自動化練習サイト](https://hotel.testplanisphere.dev/ja/)
    - いつもお世話になっております
- ChatGPTとのやりとりはModel: GPT-4で3/15~16にかけて


## E2E自動テストを書かせてみた

まずは最初の依頼。

> 次のサイトに対して、自動E2Eテストを作成したいです。
> Python, Playwright, pytestのテストコードを書いてください。
> https://hotel.testplanisphere.dev/ja/

### ChatGPTが出力したPython+Playwrightのコード

これに対する返答が以下です。

--ここからChatGPT
以下は、Python, Playwright, pytestを使った自動E2Eテストコードのサンプルです。このテストコードは、ホテル予約システムの基本的な機能をテストします。まず、必要なパッケージをインストールしてください:

``` zsh
pip install playwright pytest-playwright
```

そして、以下のテストコードをtest_hotel.pyというファイルに保存します。

``` python
import pytest
from playwright.sync_api import sync_playwright

def test_login():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto("https://hotel.testplanisphere.dev/ja/")

        # ログイン処理
        page.click("text=ログイン")
        page.fill("input[name='email']", "example@example.com")
        page.fill("input[name='password']", "password")
        page.click("text=ログイン")

        # ログイン後のページ確認
        assert page.url.endswith("/mypage.html")

        browser.close()


def test_reservation():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto("https://hotel.testplanisphere.dev/ja/")

        # ログイン処理
        page.click("text=ログイン")
        page.fill("input[name='email']", "example@example.com")
        page.fill("input[name='password']", "password")
        page.click("text=ログイン")

        # 予約処理
        page.click("text=予約管理")
        page.click("text=新規予約")
        page.select_option("select[name='plan']", "0")  # プランの選択
        page.fill("input[name='term']", "3")  # 宿泊日数入力
        page.fill("input[name='headcount']", "2")  # 宿泊人数入力
        page.click("text=確定")

        # 予約完了ページ確認
        assert page.url.endswith("/complete.html")
        assert "予約が完了しました" in page.text_content("h1")

        browser.close()
```

最後に、コマンドラインでpytestを実行してテストを実施します。

``` zsh
pytest test_hotel.py
```

--ここまでChatGPT

仕方ない点１として、ログインのメールアドレスとパスワードがexampleなどのままになっています。
HOTEL PLANISPHEREの場合はサイトにログイン情報も書いてあるので、これを使いましょう。

業務でこうした使い方をする場合も、ログイン情報などは自分で書くはずなので、これはChatGPTの落ち度ではないですね。

さて、実際にコードをコピペして動かしてみたところ、いくつか問題が。

- pytestが入っていない
- ブラウザが古い

などです。後者は、以下のようなメッセージが表示されました。

``` zsh
E       playwright._impl._api_types.Error: Executable doesn't exist at C:\Users\yoshiki.ito\AppData\Local\ms-playwright\chromium_win64_special-1050\chrome-win\chrome.exe
E       ╔════════════════════════════════════════════════════════════╗
E       ║ Looks like Playwright was just installed or updated.       ║
E       ║ Please run the following command to download new browsers: ║
E       ║                                                            ║
E       ║     playwright install                                     ║
E       ║                                                            ║
E       ║ <3 Playwright Team                                         ║
E       ╚════════════════════════════════════════════════════════════╝
```

Playwright公式の手順（[Installation | Playwright Python](https://playwright.dev/python/docs/intro)）には`pip install pytest-playwright`の後に`playwright install`するように記載されているので、ここはGPT-4書いておいてほしかったところですね。

※私の環境だと`playwright install`が動かなかった（おそらくパスの問題）ので、直接exeのパスを調べて実行しました。
``` cmd
C:\Users\yoshiki.ito\AppData\Roaming\Python\Python310\Scripts\playwright.exe install
```
なイメージ。

さて、実行した結果は・・・

``` zsh
=============================================== short test summary info ===============================================
FAILED test_hotel.py::test_login - AssertionError: assert False
FAILED test_hotel.py::test_reservation - playwright._impl._api_types.TimeoutError: Timeout 30000ms exceeded.
================================================= 2 failed in 32.44s ==================================================
```

動かず。

### エラーの原因調査

１ケース目は、操作手順としては問題ないはずなのにassert errorになっているので、待機かもしれない、と予想されます。
２ケース目も、ログイン後の予約管理クリックでタイムアウトになっています。が、**そもそもこのサイトに予約管理なんてリンク・ボタンは無い**ので、そりゃ押せません。

予約処理の部分については、どうもどこかからか適当に引っ張ってきたようです。ログインまわりを見ると実際にURL開いてhtmlも見ているようなのですが・・・厳しいか。

## 中間結論

ChatGPTにURL指定して「E2E自動テスト書いて」は、まだ早い！

ですが、あくまでも人間が見直すという前提であれば、テンプレを出力させるくらいには使えそうです。というか、すごくそれっぽいコード出してきたので「このまま動くんじゃないか」と思ってしまうくらいでした。

ただ、これだと「まだ使えないねー、はっはっは」だけで終わってしまって面白くない・・・ので、自分がテスト自動化エンジニアとして後輩に言いそうなことを言ってみることにしました。

## PageObjectPatternに沿った書き方に変えさせる

以下の指示を出してみました。

> test_hotel.pyを、ページオブジェクトパターンに則った書き方に変えてください。

### ChatGPTが出力した、PageObjectPatternに沿ったコード

ページオブジェクトパターンを使用して、テストコードをより構造化し、保守性を向上させることができます。以下の例では、ページオブジェクトクラスを作成し、テストコードを更新します。

まず、ページオブジェクトクラスを定義するために、page_objects.pyという新しいファイルを作成し、以下のコードを追加します。

``` python
class LoginPage:
    def __init__(self, page):
        self.page = page

    def login(self, email, password):
        self.page.fill("input[name='email']", email)
        self.page.fill("input[name='password']", password)
        self.page.click("text=ログイン")

    def is_logged_in(self):
        return self.page.url.endswith("/mypage.html")


class ReservationPage:
    def __init__(self, page):
        self.page = page

    def make_reservation(self, plan, term, headcount):
        self.page.select_option("select[name='plan']", plan)
        self.page.fill("input[name='term']", term)
        self.page.fill("input[name='headcount']", headcount)
        self.page.click("text=確定")

    def is_reservation_complete(self):
        return self.page.url.endswith("/complete.html") and "予約が完了しました" in self.page.text_content("h1")
```

次に、test_hotel.pyを以下のように更新します。

``` python
import pytest
from playwright.sync_api import sync_playwright
from page_objects import LoginPage, ReservationPage


def test_login():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto("https://hotel.testplanisphere.dev/ja/")

        # ログイン処理
        login_page = LoginPage(page)
        login_page.login("example@example.com", "password")

        # ログイン後のページ確認
        assert login_page.is_logged_in()

        browser.close()


def test_reservation():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto("https://hotel.testplanisphere.dev/ja/")

        # ログイン処理
        login_page = LoginPage(page)
        login_page.login("example@example.com", "password")

        # 予約処理
        reservation_page = ReservationPage(page)
        reservation_page.make_reservation("0", "3", "2")

        # 予約完了ページ確認
        assert reservation_page.is_reservation_complete()

        browser.close()
```

なにこれすごい・・・

それこそ、システムテスト自動化するのに線形スクリプティングで書かれたコードとかを与えて「リファクタして」って言ったらやれちゃいますね・・・

## fixtureを使って前処理の共通化をさせる

調子にのって頼んでみました。

> fixtureを使って、ページを開く処理を前処理として共通化してください。

### ChatGPTが書いた、fixtureで前処理を切り出したコード

pytest.fixtureを使用して、ページを開く処理を共通化し、テストケースの前処理として実行できます。test_hotel.pyを以下のように更新します。

``` python
import pytest
from playwright.sync_api import sync_playwright
from page_objects import LoginPage, ReservationPage


@pytest.fixture(scope="function")
def browser_page():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto("https://hotel.testplanisphere.dev/ja/")
        yield page
        browser.close()


def test_login(browser_page):
    # ログイン処理
    login_page = LoginPage(browser_page)
    login_page.login("example@example.com", "password")

    # ログイン後のページ確認
    assert login_page.is_logged_in()


def test_reservation(browser_page):
    # ログイン処理
    login_page = LoginPage(browser_page)
    login_page.login("example@example.com", "password")

    # 予約処理
    reservation_page = ReservationPage(browser_page)
    reservation_page.make_reservation("0", "3", "2")

    # 予約完了ページ確認
    assert reservation_page.is_reservation_complete()


@pytest.mark.parametrize("plan, term, headcount", params)
def test_reservation_combinations(browser_page, plan, term, headcount):
    # ログイン処理
    login_page = LoginPage(browser_page)
    login_page.login("example@example.com", "password")

    # 予約処理
    reservation_page = ReservationPage(browser_page)
    reservation_page.make_reservation(plan[0], term, headcount)

    # 予約完了ページ確認
    assert reservation_page.is_reservation_complete()
```

このコードは、browser_pageという名前の新しいpytest.fixtureを定義しています。このbrowser_pageは、ページを開く処理を共通化し、テストケースの前処理として実行します。各テスト関数にbrowser_pageを引数として渡すことで、ページを開いた状態でテストが始まります。yieldを使用して、テスト後にブラウザを閉じる処理も行っています。

## まとめ

書いたコードを何も考えずそのまま動かすのはできないけれど、指示側がある程度導くと初心者よりよっぽどまともな（使えそうな）コードを出してきます。
やっててちょっと引きました。