見出し画像

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


はじめに

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

少し前に社内で「テクシェア」という技術発表会があり、浜村 佳史さんが登壇した『振る舞い駆動テスト(RPA)とChatGPT』という発表にとても興味をもちました。

このやり方の良いところは日本語で書いたテストシナリオがそのまま動作する、というところにあり、異なる複数のパラメータの入力も日本語で書いたシナリオの中で行う事ができます。

ChatGPTの話は後編で語るとして、前編ではまず振る舞い駆動によるテストの自動化がどのようなものかをご紹介したいと思います。

テスト自動化環境について

今回のテスト自動化環境は開発言語にPythonを使用し、「pytest-bdd」というpytestを補助するツールを使用します。

また「Selenium」でも良いのですが、今回はブラウザを操作するために「Playwright」を使用します。

エディタは「Visual Studio Code」を使用します。

各ツールの詳細な説明は割愛しますが、配布サイトの手順に従いそれぞれインストールして貰えたらと思います。

■pytest-bdd
pip install pytest-bdd

■Playwright
pip install pytest-playwright

プロジェクトの最小構成は以下のような形になります。

projecttests
│ ├ features
│ │ └ web.featureGherkinでシナリオを定義するファイル
│ └ steps
│   └ test_web.py … テストを実装するファイル
└ pytest.inipytestの設定ファイル

※ファイルの文字コードはすべてUTF-8

「pytest.ini」ファイルには以下を記載します。

[pytest]
bdd_features_base_dir = tests/features/

これでテスト自動化環境の準備は完了です。(3ファイルだけで動くのは良いですね)

なお、振る舞い駆動の定義はGherkin(ガーキン)記法を用いるのですが、ここで少しGherkinの書き方について説明しておきます。

Gherkin(ガーキン)記法について

Gherkinは自然言語に近い形式を持っており非技術者でも理解しやすく、テストケースを明確に表現することができます。

Gherkinの主な要素は以下のとおりです。

  1. Feature:テストの目的を記載します。Featureは複数のシナリオで構成することができます。

  2. Scenario:システムの特定の機能をテストするための一連のステップを記載します。

  3. Given:テストの前提条件を記載します。

  4. When:操作またはアクションを記載します。

  5. Then:期待される結果を記載します。

  6. And:複数の操作を連続で実行する際などに使用します。

上記に沿ってGherkinで記載した「Googleで検索を行う」具体的なシナリオが以下のようになります。

# web.feature

Feature: Google検索

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

今回は「web.feature」という名前でファイルを作成していますが、「~.feature」ファイルにシナリオを記載し、そのシナリオに紐づく処理を別途実装して、テストの自動化を行うという流れに成ります。

pytest-bddを使ったテストの実装について

「pytest-bdd」はpytestで振る舞い駆動を実現するために、Gherkinで記載したシナリオとPythonのコードを紐づけてくれる役割を持っています。

使い方については配布サイトを見て貰うのが良いですが、見ても良く分からないという人のために最低限覚えておくことを解説しておきます。

まず、「pytest-bdd」を使うためには以下のimport文が必要です。

from pytest_bdd import scenario, given, when, then

そしてシナリオを指定するには@scenarioを使用します。

先ほど記載したGoogle検索のシナリオを例にすると 『Scenario: Googleで検索する』 の部分が該当するため、以下のような書き方になります。

@scenario('web.feature', 'Googleで検索する')

これらを「tests/steps」配下にある「test_web.py」に記載します。

具体的に実装の例は以下のようになります。(このスクリプトは生成AIで作ったものですが、詳しくは後編にて)

# test_web.py

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

# ブラウザの設定
@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()

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

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

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

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

# Google 検索ボタンをクリックする
@when('Google 検索ボタンをクリックする')
def step_when_search(page):
    page.get_by_label("Google 検索").first.click()

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

@given、@when、@thenに続けて括弧で「web.feature」で定義した項目を記載し、defに続いて各処理(Playwrightによるブラウザの操作)を実装して行きます。

ポイントとなるのは 『@when(parsers.parse('「{word}」と入力する'))』 の部分なのですが、「{word}」の部分は「web.feature」の『And 「テスト」と入力する』と紐づいており、鍵カッコ内の文字列を基に処理を変更することができます。

つまり、別の検索ワードで検索したい場合は「web.feature」を編集するだけで良いという事に成り、「test_web.py」は全く修正する必要がありません。

テストの実行は以下のコマンドで行います。

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 1 item

tests\steps\test_web.py .                                                                                   [100%]

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

プロジェクトの構成を整理してみる

pytestにはconftest.pyという特殊なファイルがあり、テストの前処理や後処理、共通処理などをconftest.pyで定義することで見通しが良くなります。
今回だとプロジェクトが小さいのであまり意味の無い話ですが、異なるシナリオから同じ処理を呼びたい、といった場合はconftest.pyに処理を移すというやり方が良いと思います。
改変したプロジェクトの構成と、各ファイルの中身を以下に記載します。

projecttests
│ ├ features
│ │ └ web.featureGherkinでシナリオを定義するファイル
│ └ steps
│   └ test_web.py … テストを実装するファイル
├ conftest.py … ★追加、共通処理を実装するファイル
└ pytest.inipytestの設定ファイル

↑ルート直下にconftest.pyを追加

# web.feature

Feature: Google検索

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

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

↑複数のシナリオを定義

# test_web.py

from pytest_bdd import scenarios

# ガーキンファイルの読み込み
scenarios('web.feature')

↑「web.feature」で定義した複数のシナリオをすべて実行するよう修正

# conftest.py

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

# ブラウザの設定
@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()

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

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

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

# Google 検索ボタンをクリックする
@when('Google 検索ボタンをクリックする')
def step_when_search(page):
    page.get_by_label("Google 検索").first.click()

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

↑test_web.pyから処理を移植

# pytest.ini

[pytest]
bdd_features_base_dir = tests/features/
norecursedirs = old

↑oldフォルダ配下のテストは実行しないよう修正
実行すると以下のように「web.feature」で定義した2つのテストシナリオが実行されます。

PS C:\work\testproject> pytest
============================================== 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 2 items

tests\steps\test_web.py ..                                                                                  [100%]

=============================================== 2 passed in 5.94s ================================================ 
PS C:\work\testproject> 

おわりに

後編では今回作成した「web.feature」「test_web.py」を生成AIで作る方法について解説したいと思いますが、まずは基礎となるpytest-bddを使ったテストの実装について紹介しました。

たった3つのファイルだけで環境が作れて動作するというのは導入コストも低くてとても良いですね。

SHIFT社内にはこの環境をゴリゴリにカスタマイズした凄い数の振る舞いが定義されたツールがあるのですが、最初見た時は少しハードルが高いと感じたので前編では最小限の構成で最低限のシンプルな説明にしてみました。

それでは後編もお楽しみに!「スキ」と「フォロー」もお忘れなく!

《あわせて読みたいこの公式ブロガーの記事》
\後編&改良編も公開中/


執筆者プロフィール:片山 嘉誉 (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