Behavior-Driven Development(BDD)とは
Behavior-Driven Development(BDD)とは、異なる関係者間での協力、コミュニケーション、共通理解を重視したアジャイルなソフトウェア開発手法です。2000年代初頭にTest-Driven Development(TDD)の進化版として導入されました。BDDは、開発者、テスター、ビジネス担当者がアプリケーションの期待される動作を明確かつ簡潔で曖昧でない形式で定義するために共通言語を提供することを目的としています。
BDDの主な目的は、全てのチームメンバーが要件とアプリケーションの期待される動作を理解することです。これは、ユーザーストーリーとシナリオに焦点を当てて実現されます。これらは、技術的・非技術的関係者が理解しやすい構造化された人間が読みやすい形式で書かれ、アプリケーションの動作を説明するための例を提供し、開発プロセスを推進します。
BDDのプロセス
BDDの日々の活動は、次の3つのステップで構成されます。
- ディスカバリー
- フォーミュレーション
- オートメーション
ディスカバリー: 協同的な会話
ディスカバリーフェーズは、チームメンバーがアプリケーションの期待される動作を議論して定義するBDDプロセスの始まりです。このフェーズは、開発者、テスター、ビジネス担当者が協力してコミュニケーションを取り合い、プロジェクトの要件に共通理解を持つことを保証します。
ディスカバリーでは、チームは「Three Amigos」と呼ばれる協同的な会話セッションに参加し、ユーザーストーリーとシナリオについて議論します。ユーザーストーリーは、望ましい機能の概要的な説明を提供し、シナリオは異なる状況でアプリケーションがどのように動作するかを具体的な例で説明します。これらの会話の目的は、曖昧さや誤解を特定し、期待される動作について合意に達することです。
フォーミュレーション: シナリオの記述
チームが望ましい動作を明確に理解したら、フォーミュレーションフェーズに移り、構造化された人間が読みやすい形式でシナリオを記述します。シナリオは、アプリケーションのドキュメンテーションと実行可能な仕様の両方として重要な役割を果たします。
シナリオはGiven-When-Then構文を使用して記述されます。これにより、前提条件、アクション、および期待される結果を明確かつ一貫した構造で定義できます。
- Given: システムの初期状態またはコンテキストを説明
- When: 発生するアクションまたはイベントを指定。
- Then: 期待される結果または結果を概説
この構文の使用により、技術的・非技術的関係者がシナリオを理解しやすくなります。また、シナリオは、実装の詳細ではなく、アプリケーションの動作に焦点を当てて記述するように書かれるべきです。
オートメーション: テストコードの実装
BDDプロセスの最終フェーズは、シナリオをテストコードに変換して、アプリケーションの動作を検証することです。このフェーズでは、各ステップをシナリオに対応するアプリケーションのコードにマップするコードスニペットであるステップ定義を記述します。
シナリオに基づいてテストコードを実装することで、チームはアプリケーションの動作が合意された要件に沿っていることを保証します。また、テストコードは、アプリケーションが進化するにつれて最新の状態を維持し、常に期待される動作を明確かつ正確に表現する形式のドキュメンテーションとして機能します。
アプリケーションが開発および更新されるにつれて、自動化されたテストは継続的に実行され、変更が既存の機能を壊さないことを確認することで、開発サイクルの早期に問題を特定し、解決することができるため、開発プロセスの効率性とソフトウェアの品質の向上につながります。
BDDツールとフレームワーク
BDDの実装をサポートするため、さまざまなツールやフレームワークが開発されています。これらのツールは、シナリオやテストの記述、実行、レポート作成を容易にすることで、チームがBDDのプラクティスに従いやすくなっています。
Cucumber
Cucumberは、Ruby、Java、JavaScript、Pythonなどの複数のプログラミング言語をサポートする、もっとも人気のあるBDDフレームワークの1つです。Gherkin言語を使用してシナリオを定義し、それらをステップ定義にマッピングして自動化されたテストとして実行する簡単な方法を提供します。Cucumberはさまざまなテストライブラリと統合し、プラグインをサポートして機能を拡張できます。
SpecFlow
SpecFlowは、Cucumberに密接にモデル化された.NET開発者向けのBDDフレームワークです。Gherkinを使用してシナリオを定義し、C#でステップ定義を作成できます。SpecFlowは、NUnitやxUnitなどの人気のあるテストライブラリと統合し、JenkinsやTeamCityなどの継続的インテグレーションツールと並行して使用できます。
Behat
Behatは、PHP開発者向けに特別に設計されたBDDフレームワークです。Gherkinを使用してシナリオを定義し、PHPでステップ定義を記述できます。Behatは、PHPUnitやその他のテストライブラリと統合し、PHPプロジェクトで人気があります。また、多数の拡張機能を含んでいるため、機能を拡張できます。
JBehave
JBehaveは、BDDの作成者であるDan Northによって開発されたJava開発者向けのBDDフレームワークです。Cucumberのように、Gherkinを使用してシナリオを定義し、Javaでステップ定義を書くことができます。JBehaveは、JUnitなどの人気のあるJavaテストライブラリと統合し、JenkinsやBambooなどの継続的インテグレーションツールと並行して使用できます。
Karate
Karateは、Java開発者向けのBDDフレームワークで、APIのテストに特化して設計されています。他のBDDフレームワークとは異なり、Gherkinを使用してシナリオを定義する能力と、テストを記述するための組み込みのDSL(Domain-Specific Language)を組み合わせています。これにより、テストの記述プロセスが単純化され、さまざまな技術的バックグラウンドを持つチームメンバーにとってもアクセスしやすくなります。
BDDの例
BDDのプロセスを実践的な例で説明するために、架空のオンラインバンキングアプリケーションのBDDの実装を紹介します。この例の目的は、BDDの概念やプラクティスが実際のシナリオでどのように適用されるかを示すことです。
ユーザーストーリー
プロダクトオーナーは、開発チームに次の機能を記述したユーザーストーリーを提供します。
銀行の顧客として、
自分の口座間で資金を移動したいと思っています。
これにより、自分の財務管理をより効果的に行うことができます。
ディスカバリー: コラボレーション会話
開発チームとプロダクトオーナー、品質保証担当者が「Three Amigos」と呼ばれる会話セッションを開催し、ユーザーストーリーを議論し、期待される動作を表すシナリオを特定します。
フォーミュレーション: シナリオの記述
チームは、期待される動作を説明するための次のシナリオを共同で作成します。
機能: 資金移動
シナリオ: 残高不足の口座間で資金を移動する
前提: 残高が100のチェック口座があります
かつ、残高が1000の貯蓄口座があります
もし、チェック口座から貯蓄口座に200を移動しようとする場合
ならば、移動は拒否され、
私のチェック口座の残高は100のままであり、
私の貯蓄口座の残高も1000のままです。
オートメーション: テストコードの実装
開発チームは、選択したBDDフレームワークを使用して、シナリオのステップ定義を実装します。この例では、Pythonを使用してBehaveを使用します。
from behave import given, when, then
from bank_account import Account, InsufficientFundsException
@given("残高が{balance:d}のチェック口座があります")
def step_given_checking_account_balance(context, balance):
context.checking_account = Account(balance)
@given("残高が{balance:d}の貯蓄口座があります")
def step_given_savings_account_balance(context, balance):
context.savings_account = Account(balance)
@when("チェック口座から貯蓄口座に{amount:d}を移動する場合")
def step_when_transfer_from_checking_to_savings(context, amount):
context.checking_account.transfer_to(context.savings_account, amount)
@when("チェック口座から貯蓄口座に{amount:d}を移動しようとする場合")
def step_when_attempt_transfer_from_checking_to_savings(context, amount):
try:
context.checking_account.transfer_to(context.savings_account, amount)
except InsufficientFundsException:
# 移動が拒否された
@then("私のチェック口座の残高は{expected_balance:d}であるはずです")
def step_then_checking_account_balance(context, expected_balance):
assert context.checking_account.get_balance() == expected_balance
@then("私の貯蓄口座の残高は{expected_balance:d}であるはずです")
def step_then_savings_account_balance(context, expected_balance):
assert context.savings_account.get_balance() == expected_balance
@then("移動は拒否されます")
def step_then_transfer_declined(context):
# try-exceptブロックで処理される
参考