pytest-bddを使って振る舞い駆動でテストを自動化!生成AIでテストスクリプトも作れます!(改良編)
はじめに
SHIFTで自動化アーキテクトをやっている片山 嘉誉です。
生成AIの可能性は無限大でプロンプト次第で生成される内容も変わってくるため試行錯誤に終わりが無いのですが、今回は以前使ったプロンプトを改良してpytest-bddを使った振る舞い駆動のテスト環境の利便性を向上させて行きたいと思います。
この記事はpytest-bddの基礎を解説している前編と、Playwrightのコードから生成AIでスクリプトを作成している後編があり、本編では後編で使ったプロンプトを更に改良しています。
前提となる説明は割愛していますので、まだ読んでいない方はまず前編、ならびに後編を読んで頂ければと思います。
なお、改めて補足ですが、SHIFTでは社内で誰でもGPT-4が使える環境が提供されており、今回はその環境を使ってコードの生成を行っています。
もしこの記事を読んでやってみたけど全然ダメじゃん!っと思った方はGPT-4を使ってみてください。
改良点について
おさらいとして今回使用するテスト環境の構成について記載しておきます。
■プロジェクト構成
project
├ tests
│ ├ features
│ │ └ web.feature … Gherkinでシナリオを定義するファイル
│ └ steps
│ └ test_web.py … テストを実装するファイル
├ conftest.py … 共通処理を実装するファイル
└ pytest.ini … pytestの設定ファイル
プロンプト内に具合的なファイル名を記載していますが、上記構成について全く触れなくても動くコードが生成されるのは凄いですね。
さて、今回改良するのは以下の2つになります。
conftest.pyに対する改良
後編では取り敢えず動作するソースコードを作成することに成功しましたが、前編で述べたようにpytestでは共通処理をconftest.pyで記載した方がコードがすっきりします。
人が判断して整理するのも良いですが、今回は生成AIにすべて任せて生成AI的に汎用的に使える処理だと思うものをconftest.pyに記載して貰うようにしました。
シナリオに対する改良
pytest-bddではScenario Outlinesを使って同じシナリオに対して複数のパラメータを逐次代入してテストを実施するというやり方があります。
出力されたものを手動で直しても良いのですが正直それも煩わしいので、今回はその生成も自動で行ってもらうようにしました。
書き方については公式サイトを参照するのが良いのですが、2023年9月時点で記載されている内容には不備があり、そのままコピペしただけでは動作しませんでした。(ここは私の実行環境の問題かも知れませんが…)
ポイントはこの「<>」括弧とこの「{}」括弧の表記を合わせる必要がある、という点で、どちらの括弧を使用しても動作することを確認しました。
サンプルを基に動作するよう改変したコードが以下に成ります。(本当は「<>」の方が良いのだと思いますが、諸事情により「{}」で統一しています)
■tests\features\scenario_outlines.featureファイル
# content of scenario_outlines.feature
Feature: Scenario outlines
Scenario Outline: Outlined given, when, then
Given there are {start} cucumbers
When I eat {eat} cucumbers
Then I should have {left} cucumbers
Examples:
| start | eat | left |
| 12 | 5 | 7 |
| 20 | 3 | 17 |
| 16 | 3 | 13 |
■tests\steps\test_scenario_outlines.pyファイル
# test_scenario_outlines.py
from pytest_bdd import scenarios, given, when, then, parsers
scenarios("scenario_outlines.feature")
@given(parsers.parse("there are {start} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return {"start": int(start), "eat": 0}
@when(parsers.parse("I eat {eat} cucumbers"))
def eat_cucumbers(cucumbers, eat):
cucumbers["eat"] += int(eat)
@then(parsers.parse("I should have {left} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
assert cucumbers["start"] - cucumbers["eat"] == int(left)
■実行結果(引き算をするだけのプログラムなので、引いた結果が一致していればパスします)
PS C:\work\testproject> pytest --log-cli-level=DEBUG .\tests\steps\test_scenario_outlines.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
collecting ...
----------------------------------------------- live log collection -----------------------------------------------
DEBUG parse:parse.py:371 format 'there are cucumbers' -> 'there are cucumbers'
DEBUG parse:parse.py:371 format 'I eat cucumbers' -> 'I eat cucumbers'
DEBUG parse:parse.py:371 format 'I should have cucumbers' -> 'I should have cucumbers'
collected 3 items
tests/steps/test_scenario_outlines.py::test_outlined_given_when_then[12-5-7] PASSED [ 33%]
tests/steps/test_scenario_outlines.py::test_outlined_given_when_then[20-3-17] PASSED [ 66%]
tests/steps/test_scenario_outlines.py::test_outlined_given_when_then[16-3-13] PASSED [100%]
=============================================== 3 passed in 0.03s ================================================
PS C:\work\testproject>
上記括弧の問題はいくらプロンプトを変更しても意図通りにコードを生成することができず、恐らく学習している内容に依存しているのだと思います。(公式サイトに書いてあるのに動かないというのが謎ですが…)
改良したプロンプトと生成結果について
前回はGoogle検索を行うコードから生成を行いましたが、今回はYahoo!マップで経路を検索するコードをベースに作っていきます。(Playwrightのレコーディング機能については後編を参照)
では、具体的なプロンプトと生成AIの返答を以下に記載します。(同じプロンプトでも生成される内容にはバラつきがあるので、当たりがでるまでガチャを回すか諦めて手動で実装しましょう)
■プロンプト
```
fromplaywright.sync_apiimportPage,expect
deftest_example(page:Page)->None:
page.goto("https://map.yahoo.co.jp/")
page.get_by_role("tab",name="ルート").click()
page.get_by_placeholder("スタート地点").click()
page.get_by_placeholder("スタート地点").fill("大阪駅")
page.get_by_placeholder("ゴール地点").click()
page.get_by_placeholder("ゴール地点").fill("東京駅")
page.get_by_role("button",name="ルートを検索").click()
assert"大阪駅から東京駅"inpage.title()
```
上記はpytestで書かれたPlaywrightのプログラムである。
このプログラムを以下の制約に従いPlaywrightとpytest-bddを使ったテストスクリプトに変換する。
・汎用的な処理はconftest.pyで定義する。
・ブラウザのセッション情報は引き継げるようにする。
・ブラウザの設定にsync_playwrightのchromium.launch(headless=False)を指定する。
・各操作を行った後に100ms待ってからpage.screenshotでtests/logsフォルダ配下に現在の日時とスクリーンショットを取る度に関数内で数値をインクリメントした撮影回数をファイル名にしてスクリーンショットを保存する。
・web.featureでGherkinのフォーマットに従い振る舞いを必ず日本語で定義する。
・web.featureのフォーマットを以下に示す。
```
Feature: 目的
Scenario Outline: シナリオ
Given 前提条件"{定義1}"前提条件
When 操作"{定義2}"操作
Then 結果"{定義3}"結果
Examples:
| 定義1 | 定義2 | 定義3 |
| A | B | C |
```
・web.featureでは行毎に異なる定義名を使用する。
・test_web.pyでpytestのソースコード1行に対してPlaywright、pytest-bddの振る舞いを書く。
・振る舞いはgiven、when、thenで定義され、パラメータの定義もtest_web.pyとweb.featureで関連付ける。
・振る舞いの中でテストシナリオとして変更可能な部分のみに鍵カッコを付け、テストシナリオとして変更できない部分に鍵カッコは付けない。
・変更可能な部分はtest_web.pyでparsers.parseを使って振る舞いを解析する。
・コードはまとめてを書く。
■生成結果
上記は何回かガチャを回した結果ですが、前述した括弧の問題以外コピペで動作するコードが生成できました。
conftest.pyの処理は使いまわしができる部分が綺麗に抜き出されており、とても良い結果になったと思います。
テストシナリオも生成AIで作る
正常系のパターンを作成する
ここまで来たら複数の入力値を使ってテストをしたくなりますが、これも生成AIに任せればいくらでも作る事ができます。
■プロンプト
```
# web.feature
Feature: Yahoo路線情報で大阪から東京までのルートを検索
Scenario Outline: ルートを検索
Given Yahoo地図にアクセス
When スタート地点に"{start}"を入力しゴール地点に"{goal}"を入力
Then ページタイトルに"{title}"が表示される
Examples:
| start | goal | title |
| 大阪駅 | 東京駅 | 大阪駅から東京駅 |
```
上記はpytest-bddで動作するガーキンのテストシナリオである。
Examplesの部分を改変して異なるテストシナリオを10個作成して欲しい
■生成結果
titleのところは特に何も指定していないですが、入力したサンプルから空気を読んで同じ形式で生成してくれました。
もちろん実行結果はすべてパスしました。
■実行結果
PS C:\work\testproject> pytest .\tests\steps\test_web.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 10 items
tests\steps\test_web.py .......... [100%]
============================================== 10 passed in 46.19s ===============================================
PS C:\work\testproject>
異常系のパターンを作成する
ここからは実験的なところですが、異常系のテストシナリオについても生成AIで作れるか試してみました。
期待する動作はアプリの仕様に依存するのでこのテストは結果的にすべてパスすることはありませんが、複数のパターンを自動で生成できるというのは有用であるように思いました。
■プロンプト
``` #web .feature
Feature:Yahoo路線情報で大阪から東京までのルートを検索
Scenario Outline: ルート検索に失敗
Given Yahoo地図にアクセス
When スタート地点に"{start}"を入力しゴール地点に"{goal}"を入力
Then ページ内に"{error_text}"が表示される
Examples:
| start | goal | error_text |
| @#$%^&*() | 東京駅 | 検索条件に一致する公共交通機関のルートは見つかりませんでした。 |
```
上記はpytest-bddで動作するガーキンの異常系のテストシナリオである。
Examplesの部分を改変して以下のテスト観点を盛り込んだ異なるテストシナリオを10個作成して欲しい。
・境界値(最大値)
・特殊文字
・日本語以外の言語
・SQLインジェクション
■生成結果
おわりに
前編、後編、そして改良編まで読んで頂いた方、本当にありがとう御座いました!
今回の一連の検証でPlaywrightのレコーディング機能を使って動くコードからスクリプトやシナリオを生成すれば、手間なく振る舞い駆動でのテスト自動化が実現できることが分かりました。
しかし、実際に生成AIにプロンプトを入れて検証してみると分かりますが、ある程度空気を読んでコードを生成してくれるものの、思ったコードが出てくるにはやはり何度かプロンプトを微調整しながら試す必要があります。
とあるYouTubeの動画で『外注先に仕事を依頼すると出てくる成果物が思ったものと違う時があるけど、AIに仕事をさせるのもほとんどそれと同じ。依頼する力が試される。』という話を聞きましたが、まさに今やっていることはそれです。
生成AIの性能がいくら上がってもできあがるものはあくまでこちらが入力した情報次第になるので、今後はどれだけAIに理解して貰えるプロンプトが書けるかが肝に成ってきそうですが、うまくお付き合いできるよう生成AIの気持ちを考えてあげないと行けないですね。
それではまた次の記事でお会いしましょう!「スキ」と「フォロー」もお忘れなく!
《あわせて読みたいこの公式ブロガーの記事》
お問合せはお気軽に
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:UnsplashのMarkus Spiske