見出し画像

pytest-bddを使って振る舞い駆動でテストを自動化!生成AIでテストスクリプトも作れます!(後編)


はじめに

SHIFTで自動化アーキテクトをやっている片山 嘉誉です。

前編をまだお読みで無いという方は仕組みの理解も含めてまずは前編からお読みください。

後編では前編で作成した「web.feature」「test_web.py」を生成AIでどうやって作っていくのか、というところをご紹介したいと思います。

なお、SHIFTでは社内で誰でもGPT-4が使える環境が提供されており、今回はその環境を使ってコードの生成を行っています。

ChatGPT(GPT-3.5)でも試してみたのですが動かないコードが生成されてしまうので、もしこの記事を読んでやってみたけど全然ダメじゃん!っと思った方はGPT-4を使ってみてください。

何をインプット情報にすれば生成AIは理解してくれるだろうか

振る舞い駆動ではGherkinで書かれたシナリオに対して、実際にブラウザを操作するためにはPlaywrightの実装が必要になります。

Playwrightのコードを生成AIに書いて貰おうとした場合、問題となるのが実際に操作を行うウェブサイトの構成です。

生成AIで作成したコードに対してオブジェクトに割り当てられているidや画面上に表示されている文字列を当てはめて行く、というやり方もあるかと思いますが、それだと流石に時間が掛かり過ぎて手動でコードを書くのと変わりありません。

また、最終的にはGherkinでひとつひとつの操作を定義する必要があり、どのような粒度で定義するか考えるのも時間が掛かります。

これらを生成AIで自動的に生成できるようにするにはどうすれば良いか考えた際、Playwrightのレコーディング機能で生成したコードが使えるのではないかと思い試してみました。

Playwrightのレコーディング機能について

PlaywrightにはGUI上で行った操作からコードを生成してくれる機能があります。

詳しくは公式サイトを参照して頂きたいのですが、Visual Studio Codeを使った場合はTypeScriptのコードをボタン操作で生成することができます。

しかし、試してみたもののTypeScriptのコードをpytest向けに変換した場合は動作しないコードが生成される確率が高かったので、ここは素直にpytest向けのコードを生成AIに入れてやることにしました。

pytestの場合はコマンドからGUIを起動してコードを生成する必要があるのですが、古い記事だとコマンドが違ったりしたので「playwright codegen --help」コマンドで使える引数を確認するようにしてください。

PS C:\work\testproject> playwright codegen --help
Usage: playwright codegen [options] [url]

open page and generate code for user actions

Options:
  -o, --output              saves the generated script to a file
  --target                   language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java (default: "python")
  --save-trace               record a trace for the session and save it to a file
  --test-id-attribute   use the specified attribute to generate data test ID selectors
  -b, --browser           browser to use, one of cr, chromium, ff, firefox, wk, webkit (default: "chromium")
  --block-service-workers              block service workers
  --channel                   Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc
  --color-scheme               emulate preferred color scheme, "light" or "dark"
  --device                 emulate device, for example  "iPhone 11"
  --geolocation           specify geolocation coordinates, for example "37.819722,-122.478611"
  --ignore-https-errors                ignore https errors
  --load-storage             load context storage state from the file, previously saved with --save-storage
  --lang                     specify language / locale, for example "en-GB"
  --proxy-server                specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
  --proxy-bypass               comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"
  --save-har                 save HAR file with all network activity at the end
  --save-har-glob        filter entries in the HAR by matching url against this glob pattern
  --save-storage             save context storage state at the end, for later use with --load-storage
  --timezone                time zone to emulate, for example "Europe/Rome"
  --timeout                   timeout for Playwright actions in milliseconds, no timeout by default
  --user-agent              specify user agent string
  --viewport-size                specify browser viewport size in pixels, for example "1280, 720"
  -h, --help                           display help for command

Examples:

  $ codegen
  $ codegen --target=python
  $ codegen -b webkit https://example.com

私の環境では以下の「python-pytest」を引数に入れることでレコーディング機能を呼び出す事ができました。

playwright codegen -o tests/test_chromium.py --target python-pytest https://www.google.co.jp

実行するとシークレットモードでブラウザが起動し、行った操作のコードを記録してくれます。

できたコードにアサーションを入れたら、ひとまず生成AIへのインプット情報は完成です。

from playwright.sync_api import Page, expect


def test_example(page: Page) -> None:
    page.goto("https://www.google.co.jp/")
    page.get_by_label("検索", exact=True).click()
    page.get_by_label("検索", exact=True).fill("テスト")
    page.get_by_label("Google 検索").first.click()
    assert "テスト - Google 検索" in page.title()

実際に動作するか確認したい場合は以下のコマンドで実行できます。

PS C:\work\testproject> pytest tests/test_chromium.py
============================================== test session starts ===============================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: C:\work\testproject, configfile: pytest.ini
plugins: allure-pytest-2.9.45, base-url-2.0.0, bdd-4.1.0, playwright-0.4.2, timeout-2.1.0
collected 1 item

tests\test_chromium.py .                                                                                                                                                                                                                                       [100%]

=============================================== 1 passed in 4.03s ================================================
PS C:\work\testproject> 

生成AIを使ってpytest-bddで動作するよう変換する

ここが後編の一番肝になる部分ですが、上記で作成したPlaywrightのコードを生成AIで変換して行きます。

色々プロンプトを変えて安定動作するものが生成できるようやってみたのですが、コピペだけで動作する場合もあれば一部手直しが必要な場合もありました。

ただ、手直しが必要と言っても1行~数行直すだけで動作するので、概ね精度は出ていると思います。

では、具体的なプロンプトと生成AIの返答を以下に記載します。(同じプロンプトでも生成される内容にはバラつきがあるので、当たりがでるまでガチャを回すか諦めて手動で実装しましょう)

■プロンプト(1回目)

fromplaywright.sync_apiimportPage,expect


deftest_example(page:Page)->None:
page.goto("https://www.google.co.jp/")
page.get_by_label("検索",exact=True).click()
page.get_by_label("検索",exact=True).fill("テスト")
page.get_by_label("Google検索").first.click()
assert "テスト - Google 検索" in page.title()

上記はpytestで書かれたPlaywrightのプログラムである。
このプログラムを以下の制約に従いPlaywrightとpytest-bddを使ったテストスクリプトに変換する。
・テストの実行状況を確認するため、ブラウザの設定にsync_playwrightのchromium.launch(headless=False)を指定する。
・ブラウザのセッション情報は引き継げるようにする。
・pytestのソースコード1行に対してPlaywright、pytest-bddの振る舞いを書く。
・このスクリプトはweb.feature(ガーキン)ファイルからpytestの処理順で呼び出す。
・振る舞いの定義は日本語で書く。
・可変な部分は入力値としてガーキン側で定義する。
・ガーキンファイルからの入力はparsers.parseを使って解析する。
・各操作を行った後に100ms待ってからpage.screenshotでtests/logsフォルダ配下に現在の日時とインクリメントした撮影回数をファイル名にしてスクリーンショットを保存する。
・まとめてコードを書く。
・Pythonコードのコメントは日本語で実行コマンドの例は書かない。

■生成結果(この内容を「test_web.py」に貼り付ける)

from playwright.sync_api import sync_playwright
from pytest_bdd import scenario, given, when, then, parsers
import pytest
import datetime
import time

# ブラウザの設定
@pytest.fixture(scope='session')
def browser():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        yield browser
        browser.close()

# ブラウザのセッション情報を引き継ぐ
@pytest.fixture
def page(browser):
    context = browser.new_context()
    page = context.new_page()
    yield page
    context.close()

# 撮影回数の初期設定
@pytest.fixture
def increment():
    return {'count': 0}

# スクリーンショットを保存する関数
def save_screenshot(page, increment):
    now = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    increment['count'] += 1
    page.screenshot(path=f"tests/logs/{now}_{increment['count']}.png")

# ガーキンファイルの読み込み
@scenario('web.feature', 'Googleで「テスト」を検索する')
def test_web():
    pass

# Googleのトップページにアクセスする
@given('Googleのトップページにアクセスする')
def step_given(page):
    page.goto("https://www.google.co.jp/")
    save_screenshot(page)

# 「検索」ボタンをクリックする
@when('「検索」ボタンをクリックする')
def step_when(page, increment):
    page.get_by_label("検索", exact=True).click()
    time.sleep(0.1)
    save_screenshot(page, increment)

# 「テスト」と入力する
@when(parsers.parse('「{word}」と入力する'))
def step_when_input(page, word, increment):
    page.get_by_label("検索", exact=True).fill(word)
    time.sleep(0.1)
    save_screenshot(page, increment)

# 「Google 検索」ボタンをクリックする
@when('「Google 検索」ボタンをクリックする')
def step_when_search(page, increment):
    page.get_by_label("Google 検索").first.click()
    time.sleep(0.1)
    save_screenshot(page, increment)

# 検索結果のタイトルが正しいことを確認する
@then(parsers.parse('検索結果のタイトルが「{title}」であることを確認する'))
def step_then(page, title):
    assert page.title() == title
    save_screenshot(page)

■プロンプト(2回目)

web.featureを生成して

■生成結果(この内容を「web.feature」に貼り付ける)

以下がweb.featureの例です。

Feature: Google検索

  Scenario: Googleで「テスト」を検索する
    Given Googleのトップページにアクセスする
    When 「検索」ボタンをクリックする
    And 「テスト」と入力する
    And 「Google 検索」ボタンをクリックする
    Then 検索結果のタイトルが「テスト - Google 検索」であることを確認する

このファイルはプロジェクトのルートディレクトリに配置します。

これ、何が凄いって「テスト」など後から可変と成り得る箇所がGherkinだけ変更すれば良いようコードが生成されている点です。

具体的な指示を出していないのに空気を読んで実装してくれる辺り、GPT-4がとても優秀であることが分かりました。

残念な事に上記のコードはコピペしただけだと動作しませんでしたが、一部修正するだけで意図通り動いたので十分実用的であると思います。

おわりに

後編ではPlaywrightのコードをpytest-bddで動作できるよう変換してみましたが、たった7行のコードから約10倍の67行もコードを書いてくれるのですから、これは活用しない手は無いと思います。

Gherkinによるシナリオの管理は可読性が良いのでとても有用だと思いますし、生成AIを使えば様々なパターンのシナリオ作成も出来るかも知れません。

今回作成したテストスクリプトをベースに様々なシナリオを生成AIで作れるようになれば、テストの幅が広がりそうですね。(次の課題にしたいと思います)

それではまた次の記事でお会いしましょう!「スキ」と「フォロー」もお忘れなく!

《併せて読みたい関連記事》


執筆者プロフィール:片山 嘉誉 (Katayama Yoshinori)
SIerとして長年AOSP(Android Open Source Project)をベースとしたAndroidスマートフォンの開発に携わり、併せてスマートフォン向けのアプリ開発を行ってきた。
Wi-FiやBluetoothといった無線通信機能の担当が長かったため通信系のシステムに強く、AWSを使用したシステム開発の経験もある。
生産性向上や自動化というワードが大好きで、SHIFTでは自動化アーキテクトとして参画。
最近のマイブームはChatGPTとStable Diffusionである。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら

PHOTO:UnsplashMarkus Spiske