Gherkin + pytest-bdd でBDDを試してみた
背景・目的
結合試験や総合試験の試験仕様書は、Excelで作成・管理されることが多いと思います。試験項目・手順・期待結果・判定を列に並べる形式で、人間が操作して目で確認し判定する前提で成立してきました。
現在では、LLMがシナリオを読み込んでテストコードを書き、実行して結果を判定できます。生成はAmazon Q Developerの /test のように機能として提供されています。実行までを含む自律的な動きも、各社が公式に説明しています。GitHubのドキュメントには次のようにあります。
While working on a coding task, Copilot coding agent has access to its own ephemeral development environment, powered by GitHub Actions, where it can explore your code, make changes, execute automated tests and linters and more.
コーディングタスクの作業中、エージェントはGitHub Actions上の使い捨て開発環境を持つ。そこでコードを調べ、変更を加え、自動テストやリンターを実行できる、という説明です。
ところが、Excelのセルに入った仕様は機械にそのまま渡せません。人が読み替えるか、別にスクリプトを書くことになります。
もっとも、Excelには承認フローや証跡添付といった役割があります。置き換える話ではなく、機械が実行する仕様だけをテキストに寄せる試みです。
そこで、試験仕様を書く順番を逆にします。人間が読む体裁を整えてから機械に合わせるのではなく、最初から機械が実行できる形式で書き、人間はそれを読む側に回る。この機械ファーストの前提に立つと、仕様書は体裁を持った成果物ではなく実行可能なテキストになります。具体的には、BDD(Behaviour-Driven Development)のGherkin記法で「何を保証するか」をプレーンテキストで書きます。そしてpytest-bddで、その各行をPython処理へ紐付けます。
この記事では、PoCとして本番想定のAmazon Redshiftへは接続せず、SQLite(インメモリ)と一時CSVで済ませます。「Gherkin -> pytest-bdd -> Step Definition -> Python処理 -> assert -> pytest結果」という一連の流れが成立するかを試します。Step Definitionは、Gherkinで書いた1行を実際のPython処理に対応づける関数です。実際に手元で動かし、3シナリオのPASSと、わざと壊したときのFAILまで確認します。
まとめ
| 項目 | 内容 |
|---|---|
| これは何 | Gherkinで試験仕様を書き、pytest-bddから既存Python処理を呼んでOK/NGを自動判定する最小PoC |
| 何ができる | ①試験仕様を自然言語に近い形で記述 ②Given/When/ThenをPython関数へ紐付け ③バッチ実行とDB/ファイル確認をassertで自動判定 ④pytest結果でシナリオ別の成否を確認 |
| 基本方針 | Gherkinには「何を保証するか」だけを書く。SQL・関数名・接続方法などHowはPython側(Step Definition / helper)に置く |
| 検証したスコープ | Gherkinからassertまでの配線。対象はバッチ処理1本で、構造としては機能テスト。システム間の結合試験そのものではない |
| つまずきやすい点 | Gherkinの1行が別々のPython関数になるため、ステップ間の値の受け渡しはfixture経由になる |
| 検証状況 | 実行済み。3 passed を確認。わざと壊して 1 failed, 2 passed も確認 |
概要
BDDとは
BDD(Behaviour-Driven Development)は開発プロセスの名前です。標準化団体や公式仕様はなく、原典は提唱者Dan Northが2006年に Better Software 誌へ書いた記事です。そこではBDDが生まれた経緯が次のように説明されています。
My response is behaviour-driven development (BDD). It has evolved out of established agile practices and is designed to make them more accessible and effective for teams new to agile software delivery.
BDDは既存のアジャイル実践から発展したものであり、アジャイル開発に不慣れなチームでもそれを扱いやすく効果的にするために設計された、という説明です。
ツール側の説明も見ておきます。Cucumber公式には次のようにあります。
BDD is a way for software teams to work that closes the gap between business people and technical people
BDDはビジネス側と技術側の隔たりを埋めるためのチームの働き方である、という説明です。その手段として、システムにどう振る舞ってほしいかを示す具体例を中心に共同作業を進めます。
なお、Cucumberはこの BDD を実践するために作られたツールです。Gherkinはそこから生まれた記法で、今回使うpytest-bddも同じ記法を解釈します。
日々の活動は3つの実践に分かれます。
| 実践 | 内容 |
|---|---|
| Discovery | 変更の具体例について会話し、何が期待されているかを合意する |
| Formulation | その例を、自動化できる形式で文書化する |
| Automation | 文書化した例をテストとして接続し、実装を導く |
この記事が扱うのはFormulationとAutomationです。Gherkinが「自動化できる形式での文書化」に当たり、pytest-bddが「テストとして接続する」部分を担います。
Gherkinとは
Gherkinは、システムの振る舞いを自然言語に近い形で書くための記法です。空行でない各行はキーワードで始まり、そのキーワードが行の意味を決めます。公式リファレンスには次のようにあります。
Each line that isn't a blank line has to start with a Gherkin keyword, followed by any text you like.
空行でない各行はGherkinのキーワードで始まり、その後ろには任意のテキストを書く、という説明です。
主なキーワードは次のとおりです。
| キーワード | 役割 |
|---|---|
Feature |
機能の名前。1ファイルに1つだけ置ける |
Scenario(Example) |
具体例1件。ステップを並べて書く |
Given |
試験開始時点の前提状態・入力条件 |
When |
試験対象となる操作・イベント |
Then |
期待される振る舞い・結果 |
And / But
|
直前と同じ種類の条件・操作・期待結果を追加する |
Background |
全Scenarioの前に実行する共通の Given
|
Scenario Outline と Examples
|
同じ手順を表の値で繰り返す |
Rule |
業務ルール単位でScenarioをまとめる(Gherkin 6以降) |
今回のPoCで使うのは Feature / Scenario / Given / When / Then / And の6つだけです。1つのScenarioのステップ数は3から5が推奨とされています。
Cucumber公式には注意点が1つ挙げられています。ステップ定義を探すときキーワードは考慮されないため、Given と Then に同じ文を書くと重複と判定されるという内容です。「Given 残高がある」と「Then 残高がある」は区別されません。前提と結果で同じことを書きたくなったら、文を具体化して区別します。「Given 残高が430円である」「Then 残高が430円である」のようにです。
これはCucumberの仕様についての記述で、pytest-bddで同じ挙動になるかは筆者未検証です。ただGherkinの書き方としては、前提と結果を別の文にした方が仕様として読みやすくなります。
言語ごとの実行ツール
Gherkin自体は言語仕様なので、実行側は言語ごとに別のツールが担います。Cucumber公式のインストールページに、言語別の実装一覧があります。各実装は公式(official)・準公式(semi-official)・非公式(unofficial)・メンテナンス停止(unmaintained)に分類されています。主なものを挙げます。
| 言語 | ツール | 公式分類 |
|---|---|---|
| Python | pytest-bdd、Behave | いずれも準公式 |
| JavaScript | Cucumber-JS | 公式 |
| Java / Kotlin | Cucumber-JVM | 公式 |
| Ruby | Cucumber-Ruby | 公式 |
| PHP | Behat | 準公式 |
| .NET(C# / F# / VB) | Reqnroll、Xunit.Gherkin.Quick | いずれも準公式 |
| Go | gocuke | 準公式 |
Goの Godog は一覧で unmaintained(official だがメンテナンスされておらず新しいメンテナを必要としている)に分類されています。採用時は言語だけでなく、この分類も確認した方が安全です。
Pythonに限ると、選択肢は pytest-bdd と Behave の2つです。Behaveは behave コマンドという独自のランナーを持ちます。feature・steps・environment.py という構成で完結します。一方のpytest-bddは独自ランナーを持たず、既存のpytestの上で動きます。既存のpytest資産やfixtureを再利用したい今回の目的には、pytest-bddの方が噛み合います。
用語の整理
Gherkin以外に、この記事で繰り返し出てくる用語を整理します。
- Gherkin: Given / When / Then などのキーワードで振る舞いを記述する言語。
.featureファイルに書く - feature ファイル: Gherkinで書いた試験仕様そのもの。1ファイル1 Featureが基本
- pytest-bdd: Gherkinのステップをpytestのテストとして実行するプラグイン。専用ランナーは不要でpytestの仕組みをそのまま使える
- Step Definition: Gherkinの各行(ステップ)に対応するPython関数。
@given/@when/@thenで紐付ける - fixture: pytestの依存注入の仕組み。前提状態(DB接続・テストデータ)の準備と後片付けに使う
- helper: SQL実行やファイル比較などの処理本体。Step Definitionから呼び出す
pytest-bddはBDDツールの中でも独立したランナーを持たず、pytestのfixtureをそのまま再利用できる点が特徴です。公式ドキュメントには次のようにあります。
Unlike many other BDD tools, it does not require a separate runner and benefits from the power and flexibility of pytest.
多くのBDDツールと違い専用ランナーを必要とせず、pytestの機能と柔軟性をそのまま活かせる、という説明です。
役割の分担
BDDで試験を書くときの役割分担は次のとおりです。Gherkinは「何を保証するか(What)」に徹し、「どう実現するか(How)」はPython側に寄せます。
| レイヤー | 書くもの | 書かないもの |
|---|---|---|
| feature(Gherkin) | 前提条件・操作・期待結果 | SQL・関数名・接続方法・比較アルゴリズム |
| Step Definition | ステップとPython処理の橋渡し、assert | 大量のロジック(helperへ分離) |
| helper | DB操作・ファイル操作・バッチ本体 | 試験の意図(featureに書く) |
| fixture | 前提状態の準備と後片付け | 試験ごとの個別判定 |
つながり
Gherkinの1行がpytestのテスト結果になるまでの流れです。
pytest-bddは scenarios() または @scenario でfeatureファイルをテスト関数に束ねます。そして各ステップ文字列を、@given / @when / @then で登録した関数にマッチさせます。関数の中でhelperを呼び、最後に assert でOK/NGを判定します。判定結果はpytestの passed / failed としてそのまま出ます。
実践
以下は筆者環境で実際に実行した手順と出力です。検証環境は次のとおりです。
| 項目 | バージョン |
|---|---|
| OS | macOS(darwin) |
| Python | 3.12.13 |
| pytest | 9.1.1 |
| pytest-bdd | 9.0.0 |
pytest-bdd 9.0.0 はPython 3.9のサポートを終了しており、対応バージョンは3.10から3.14です。macOSに同梱のPythonが3.9系の場合は、3.10以上を別途用意する必要があります。
1. pytest-bddをインストールする
-
仮想環境を作って有効化します。
% python3 -m venv .venv % . .venv/bin/activate ((.venv) ) % -
pytestとpytest-bddをインストールします。
requirements.txtはプロジェクトのルート直下(tests/やpytest.iniと同じ階層)に置きます。中身は次の2行だけです。((.venv) ) % cat requirements.txt pytest pytest-bdd ((.venv) ) %((.venv) ) % pip install -r requirements.txt
公式のインストール手順は pip install pytest-bdd です。
2. ディレクトリ構成を作る
-
次の最小構成を作ります。featureは
tests/features/に、Step Definitionはtests/step_defs/に置きます/Users/XXXXX/XXXXX ├── pytest.ini └── tests/ ├── features/customer_batch.feature ├── step_defs/test_customer_batch.py └── helpers/batch_helper.py
3. featureファイルに試験仕様を書く
-
tests/features/customer_batch.featureに、保証したい振る舞いだけをGherkinで書きます。SQLや関数名は一切書きませんFeature: 顧客バッチ連携 Scenario: 既存顧客を更新する Given データベースに顧客Aが登録されている And 入力ファイルに顧客Aの更新データが存在する When 顧客更新バッチを実行する Then 顧客Aが入力ファイルの内容で更新される And エラー件数が0件である Scenario: 不正な顧客データを処理する Given 入力ファイルに必須項目が欠損した顧客データが存在する When 顧客更新バッチを実行する Then 不正データは登録されない And エラーとして検出される Scenario: 複数件の顧客データを処理する Given 入力ファイルに複数件の顧客データが存在する When 顧客更新バッチを実行する Then 処理対象件数と処理結果件数が一致する And 重複した顧客データは作成されない
Gherkinでは1ファイルに1つのFeatureだけを書きます。公式ドキュメントには次のようにあります。
Note that only one feature is allowed per feature file.
1つのfeatureファイルには1つのfeatureしか置けない、という注記です。
4. helperに処理本体を置く
-
tests/helpers/batch_helper.pyに、DB操作とバッチ本体を実装します。本番はRedshiftを想定していますが、PoCでは接続せずにDBはSQLite(インメモリ)、入力は一時CSVにします
"""顧客更新バッチの簡易実装とヘルパー。
本番想定のAmazon Redshiftには接続せず、SQLite(インメモリ)と
一時CSVファイルで BDD -> pytest-bdd -> Python処理 -> assert の
一連の流れを検証するための最小実装。
"""
import csv
import sqlite3
from dataclasses import dataclass, field
@dataclass
class BatchResult:
target_count: int = 0
updated_count: int = 0
error_count: int = 0
errors: list = field(default_factory=list)
def create_db():
conn = sqlite3.connect(":memory:")
conn.execute(
"CREATE TABLE customer ("
" id TEXT PRIMARY KEY,"
" name TEXT NOT NULL,"
" email TEXT NOT NULL"
")"
)
conn.commit()
return conn
def insert_customer(conn, cust_id, name, email):
conn.execute(
"INSERT INTO customer (id, name, email) VALUES (?, ?, ?)",
(cust_id, name, email),
)
conn.commit()
def get_customer(conn, cust_id):
cur = conn.execute(
"SELECT id, name, email FROM customer WHERE id = ?", (cust_id,)
)
row = cur.fetchone()
if row is None:
return None
return {"id": row[0], "name": row[1], "email": row[2]}
def count_customers(conn):
cur = conn.execute("SELECT COUNT(*) FROM customer")
return cur.fetchone()[0]
def count_input_rows(path):
"""入力CSVの行数を数える。バッチの内部カウンタとは独立に数えるため、
検証側はこちらを正とできる。"""
with open(path, newline="", encoding="utf-8") as f:
return sum(1 for _ in csv.DictReader(f))
def write_input_file(path, rows):
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["id", "name", "email"])
writer.writeheader()
for row in rows:
writer.writerow(row)
def _is_valid(row):
for key in ("id", "name", "email"):
if not row.get(key):
return False
return True
def run_customer_batch(conn, input_path):
"""入力CSVを1行ずつ読み、妥当な行はupsert、欠損行はエラーとして数える。"""
result = BatchResult()
with open(input_path, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
result.target_count += 1
if not _is_valid(row):
result.error_count += 1
result.errors.append({"row": row, "reason": "missing_required"})
continue
conn.execute(
"INSERT INTO customer (id, name, email) VALUES (:id, :name, :email) "
"ON CONFLICT(id) DO UPDATE SET name = excluded.name, email = excluded.email",
row,
)
result.updated_count += 1
conn.commit()
return result
処理本体をhelperに寄せることで、Step Definition側はロジックで膨らまず、試験の意図が読み取りやすくなります。
5. Step DefinitionでGherkinとPython処理を紐付ける
tests/step_defs/test_customer_batch.py に、Gherkinの各行に対応するPython関数を書きます。先に仕組みを3つ押さえておきます。
Gherkinの1行がPython関数1つになる
Scenario 1は5行なので、5つの別関数になります。
Given データベースに顧客Aが登録されている -> given_existing_customer()
And 入力ファイルに顧客Aの更新データが存在する -> given_update_file()
When 顧客更新バッチを実行する -> when_run_batch()
Then 顧客Aが入力ファイルの内容で更新される -> then_customer_updated()
And エラー件数が0件である -> then_no_error()
ここで問題が出ます。別々の関数なのでローカル変数を共有できません。しかし実際には共有が必要です。1番目でINSERTしたデータを4番目でSELECTして確認したい。2番目で作ったCSVのパスを3番目のバッチに渡したい。3番目の実行結果を5番目の判定に使いたい。いずれも関数をまたぎます。
fixtureで値を共有する
これを解決するのがpytestのfixtureです。@pytest.fixture を付けた関数を定義しておくと、テスト関数の引数にその名前を書くだけでpytestが値を渡してくれます。重要なのは、同じテストの中では同じオブジェクトが渡る点です。公式ドキュメントには次のようにあります。
Fixtures are evaluated only once within the PyTest scope and their values are cached.
fixtureはpytestのスコープ内で一度だけ評価され、値はキャッシュされる、という説明です。
この性質を使い、今回は2つのfixtureを用意します。db はDB接続を全ステップへ渡すためのものです。yield の後ろに後片付けを書くと、シナリオ終了後に実行されます。context は空の辞書を返すだけのfixtureです。入力ファイルのパスやバッチ実行結果といった「実行時に決まる値」を、ステップ間で受け渡すために使います。同じ辞書が全ステップに渡るので、前のステップが書いた値を後のステップが読めます。
scenarios() がfeatureをテストに変える
3つめが scenarios() です。この1行がfeatureファイルを読み、Scenarioごとのテスト関数を自動生成してこのモジュールに登録します。そのため、このファイルに def test_xxx() を1つも書いていないのに3つのテストが走ります。引数はfeatureファイルへのパスで、このPythonファイルの場所を基準にした相対パスです。
| 名前 | 正体 | 役割 |
|---|---|---|
scenarios() |
pytest-bddの関数 | featureの各ScenarioをPythonのテストに変換する |
db fixture |
自分で定義した関数 | 全ステップに同じDB接続を渡し、終了後に閉じる |
context fixture |
自分で定義した関数 | 全ステップに同じ辞書を渡し、値の受け渡しに使う |
tmp_path |
pytest標準のfixture | テストごとに使い捨ての一時ディレクトリを渡す |
コード
-
以上を踏まえたコードは下記になります
"""顧客バッチ連携のStep Definition。""" import os import sys import pytest from pytest_bdd import scenarios, given, when, then sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "helpers")) import batch_helper as bh scenarios("../features/customer_batch.feature") @pytest.fixture def db(): conn = bh.create_db() yield conn conn.close() @pytest.fixture def context(): return {} @given("データベースに顧客Aが登録されている") def given_existing_customer(db): bh.insert_customer(db, "A", "顧客A", "a-old@example.com") @given("入力ファイルに顧客Aの更新データが存在する") def given_update_file(tmp_path, context): path = tmp_path / "input.csv" bh.write_input_file(path, [{"id": "A", "name": "顧客A", "email": "a-new@example.com"}]) context["input_path"] = str(path) @when("顧客更新バッチを実行する") def when_run_batch(db, context): context["result"] = bh.run_customer_batch(db, context["input_path"]) @then("顧客Aが入力ファイルの内容で更新される") def then_customer_updated(db): cust = bh.get_customer(db, "A") assert cust["email"] == "a-new@example.com" @then("エラー件数が0件である") def then_no_error(context): assert context["result"].error_count == 0 @given("入力ファイルに必須項目が欠損した顧客データが存在する") def given_invalid_file(tmp_path, context): path = tmp_path / "input.csv" bh.write_input_file(path, [{"id": "B", "name": "顧客B", "email": ""}]) context["input_path"] = str(path) @then("不正データは登録されない") def then_invalid_not_registered(db): assert bh.get_customer(db, "B") is None @then("エラーとして検出される") def then_detected_as_error(context): assert context["result"].error_count >= 1 @given("入力ファイルに複数件の顧客データが存在する") def given_multi_file(tmp_path, context): path = tmp_path / "input.csv" bh.write_input_file( path, [ {"id": "C1", "name": "顧客C1", "email": "c1@example.com"}, {"id": "C2", "name": "顧客C2", "email": "c2@example.com"}, {"id": "C3", "name": "顧客C3", "email": "c3@example.com"}, {"id": "C1", "name": "顧客C1改", "email": "c1-new@example.com"}, ], ) context["input_path"] = str(path) # 期待する最終状態。C1は後勝ちで上書きされ、C2・C3は自分の値のまま context["expected_final"] = { "C1": "c1-new@example.com", "C2": "c2@example.com", "C3": "c3@example.com", } @then("処理対象件数と処理結果件数が一致する") def then_counts_match(context): result = context["result"] # バッチの内部カウンタ同士ではなく、入力ファイルを独立に数えて突合する input_rows = bh.count_input_rows(context["input_path"]) assert result.target_count == input_rows assert result.updated_count + result.error_count == input_rows @then("重複した顧客データは作成されない") def then_no_duplicates(db, context): expected = context["expected_final"] assert bh.count_customers(db) == len(expected) # 件数だけでなく、どのIDにどの値が入ったかまで確認する for cust_id, email in expected.items(): assert bh.get_customer(db, cust_id)["email"] == email
tmp_path はpytest標準のfixtureなので自分で定義する必要はありません。入力CSVの置き場所としてそのまま使っています。
Scenario 3の Then には気をつけた点があります。「処理対象件数と処理結果件数が一致する」を素直に書くと assert result.target_count == result.updated_count + result.error_count になります。しかしこれは必ず通ります。helperは1行読むごとに target_count を1、updated_count か error_count のどちらかを1増やす作りなので、足し算が一致するのは構造から確定しているからです。
そこで比較相手を、バッチの外にある入力ファイルの実際の行数に変えました。読み取りを途中で打ち切って行を取りこぼすバグを検出できます。ただしカウンタはDBに書けたかに関係なく加算されるため、書き込みの正しさは次の Then が担います。
「重複した顧客データは作成されない」も、件数だけ見ると id TEXT PRIMARY KEY が保証する内容を確認するだけです。顧客C1改 を誤ってC2の行に書き込むバグがあっても3件のままPASSします。そこで、どのIDにどの値が入ったかまで確認しました。期待する最終状態は Given で context に置き、Gherkinには値を書いていません。
内部の値同士を比べるassertは、実装の構造から真になりがちです。入力ファイルや期待値テーブルなど、バッチの外にある情報と突合すると検証になります。
公式が示しているのは、@given の戻り値を後続ステップへ渡すのに target_fixture="..." を使う方法です。今回は複数の値を受け渡すため、辞書 context に入れる方式にしました。1つの値を渡すだけなら target_fixture の方が簡潔です。
6. pytest.iniを置く
-
pytest.iniでテストの探索先を指定します。必須の記述はなく、[pytest]セクションだけでも動きます[pytest] testpaths = tests/step_defs -
featureファイルの探索基準は、既定でそのモジュール自身の場所です。
scenarios()にStep Definitionファイルからの相対パス(../features/...)を渡す今回の書き方なら、bdd_features_base_dirは不要です。公式ドキュメントには次のようにありますBy default, pytest-bdd will use the current module's path as the base path for finding feature files, but this behaviour can be changed in the pytest configuration file (i.e. pytest.ini, tox.ini or setup.cfg) by declaring the new base path in the bdd_features_base_dir key. The path is interpreted as relative to the pytest root directory.
既定ではfeatureファイルを探す基準はそのモジュール自身の場所であり、
bdd_features_base_dirを書けば変更できる。その値はpytestのルートディレクトリからの相対として解釈される、という説明です。基準をルートディレクトリ側へ寄せる書き方もあります。
bdd_features_base_dir = tests/featuresを設定し、scenarios("customer_batch.feature")と呼ぶ形です。どちらか一方に決めておくと、パスの基準を追うときに迷いません。
7. pytestで実行する
-
プロジェクトのルートで次のコマンドを実行します
pytest -v -
3つのScenarioがそれぞれ1つのテストとして実行されます。すべて成功すると
3 passedと表示され、-vを付けているのでScenarioごとにPASSEDが並びます。どのシナリオが成功・失敗したかはこの出力で確認できます============================= test session starts ============================== platform darwin -- Python 3.12.13, pytest-9.1.1, pluggy-1.6.0 -- /XXXXXXX/.venv/bin/python cachedir: .pytest_cache rootdir: XXXXXXXX configfile: pytest.ini testpaths: tests/step_defs plugins: bdd-9.0.0 collecting ... collected 3 items tests/step_defs/test_customer_batch.py::test_既存顧客を更新する PASSED [ 33%] tests/step_defs/test_customer_batch.py::test_不正な顧客データを処理する PASSED [ 66%] tests/step_defs/test_customer_batch.py::test_複数件の顧客データを処理する PASSED [100%] ============================== 3 passed in 0.02s =============================== -
テスト名が
test_既存顧客を更新するのように、Gherkinに書いたScenario名からそのまま生成されています。日本語のScenario名がテスト名になるため、どの試験項目が通ったかを出力だけで追えます
8. わざと失敗させて判定を確かめる
-
assertが効いていることを確認するため、期待値を意図的に変えます。たとえば正常系の検証を次のように壊します。
@then("顧客Aが入力ファイルの内容で更新される") def then_customer_updated(db): cust = bh.get_customer(db, "A") assert cust["email"] == "存在しない値@example.com" -
再度
pytest -vを実行すると、該当ScenarioがFAILEDになり、失敗したassertの行と期待値・実際値が表示されます。期待値を戻せば再び3 passedに戻ることを確認します。tests/step_defs/test_customer_batch.py:49: AssertionError =========================== short test summary info ============================ FAILED tests/step_defs/test_customer_batch.py::test_既存顧客を更新する - Asse... ========================= 1 failed, 2 passed in 0.05s ==========================
考察
Gherkinに「何を保証するか」だけを書くと、SQLや接続方法を知らない人でも試験の意図を追えます。支えるのは役割の分離です。DB操作やファイル比較をhelperへ寄せれば、既存のpytest資産やfixtureをそのまま再利用できます。大量データや詳細な期待値も同じ理由でGherkinには書かず、CSVやfixtureへ持たせます。
動かして分かったのは、結果の読みやすさです。テスト名がScenario名から生成され、test_既存顧客を更新する と日本語で出ます。Excelの試験項目書で「No.3がNG」と記録していた情報が、実行結果にそのまま残ります。
判定そのものはpytestの機能です。期待値を壊せば 1 failed, 2 passed と行番号まで出ます。BDDのために別のランナーを覚える必要がなく、既存のpytest資産をそのまま使えることが、pytest-bddを選ぶ理由になります。
今回確認できたのは、Gherkinからassertまでの配線です。対象はバッチ処理1本で、入力CSVもバッチも試験側のプロセス内にあります。構造としては機能テストであり、システム間でデータが渡る結合試験そのものではありません。
今後は、ローカルのPoCではなく、実環境で試してみたいと思います。
参考