はじめに

TDD(テスト駆動開発)を実践していると、「テストは書いているが、仕様として読みづらい」「開発者以外がテストを理解できない」という課題に直面することがあります。技術的には正しいテストでも、ビジネス要件との対応が見えにくくなってしまうのです。

BDD(Behavior-Driven Development:振る舞い駆動開発) は、この課題を解決するために生まれた開発手法です。2003年にDan North氏がTDDの改良版として提唱し、「ビジネス価値に焦点を当てたテスト」という新しい視点をもたらしました。

本記事では、BDDとTDDの本質的な違い、Gherkin記法によるシナリオ作成、そして両者を組み合わせた効果的な開発フローまで、実践的なコード例とともに解説します。この記事を読み終える頃には、仕様として読めるテストを書き、開発者とビジネスサイドの共通言語を構築できるようになっているでしょう。

BDDとは何か

BDD(Behavior-Driven Development)とは、ソフトウェアの振る舞いを自然言語に近い形式で記述し、それを実行可能な仕様として活用する開発手法です。

BDDの定義と背景

Cucumber公式ドキュメントによると、BDDは以下の3つの要素でビジネスと技術の間のギャップを埋めます。

  1. 役割を越えたコラボレーション: 解決すべき問題について共通理解を構築する
  2. 迅速で小さなイテレーション: フィードバックと価値の流れを加速させる
  3. 自動チェック可能なドキュメント: システムの振る舞いを検証可能な形式で文書化する

BDDは「テストを書く」ことよりも「会話を促進する」ことに重点を置いています。テストコードはその会話の副産物であり、実行可能な仕様書として機能します。

BDDの3つのプラクティス

BDDは日々の活動において、以下の3つのプラクティスを繰り返し実践します。

graph LR
    subgraph "Discovery"
        A[What it could do<br/>何ができるか]
    end
    subgraph "Formulation"
        B[What it should do<br/>何をすべきか]
    end
    subgraph "Automation"
        C[What it actually does<br/>実際に何をするか]
    end
    
    A --> B
    B --> C
    C --> A
    
    style A fill:#e3f2fd,stroke:#1565c0,color:#000000
    style B fill:#fff3e0,stroke:#e65100,color:#000000
    style C fill:#e8f5e9,stroke:#2e7d32,color:#000000
プラクティス 目的 成果物
Discovery 具体例を通じて要件を探索し、共通理解を構築 具体的なシナリオ例
Formulation 例を構造化された文書として形式化 Gherkin形式の仕様書
Automation 形式化した例を自動テストとして実装 実行可能な受け入れテスト

BDDとTDDの本質的な違い

BDDとTDDは密接に関連していますが、その焦点と目的には明確な違いがあります。

比較表:BDD vs TDD

観点 TDD(テスト駆動開発) BDD(振る舞い駆動開発)
起点 技術的な機能要件 ビジネス価値・ユーザーストーリー
記述言語 プログラミング言語 自然言語に近い形式(Gherkin等)
読者 開発者 開発者、QA、ビジネスサイド
粒度 ユニット(関数・メソッド) 振る舞い(ユーザーシナリオ)
目的 正しく動作するコードの作成 正しい振る舞いの定義と検証
アサーション 技術的な期待値検証 ビジネス的な期待結果の検証

アプローチの違いを図解

graph TB
    subgraph "TDD: Inside-Out アプローチ"
        T1[ユニットテスト作成]
        T2[最小限の実装]
        T3[リファクタリング]
        T4[統合テストへ拡大]
        T1 --> T2 --> T3 --> T4
    end
    
    subgraph "BDD: Outside-In アプローチ"
        B1[ユーザーストーリー定義]
        B2[シナリオ作成]
        B3[ステップ定義実装]
        B4[内部実装詳細化]
        B1 --> B2 --> B3 --> B4
    end
    
    style T1 fill:#ffcccc,stroke:#cc0000,color:#000000
    style T2 fill:#ccffcc,stroke:#00cc00,color:#000000
    style T3 fill:#cce5ff,stroke:#0066cc,color:#000000
    style T4 fill:#f0f0f0,stroke:#666666,color:#000000
    
    style B1 fill:#e3f2fd,stroke:#1565c0,color:#000000
    style B2 fill:#fff3e0,stroke:#e65100,color:#000000
    style B3 fill:#e8f5e9,stroke:#2e7d32,color:#000000
    style B4 fill:#f0f0f0,stroke:#666666,color:#000000

視点の違い:「どう動くか」vs「何をすべきか」

TDDでは「この関数は引数3に対して"Fizz"を返す」という実装の検証に焦点を当てます。

1
2
3
4
5
6
// TDDスタイル:技術的な視点
@Test
void shouldReturnFizzWhenDivisibleByThree() {
    FizzBuzz fizzBuzz = new FizzBuzz();
    assertEquals("Fizz", fizzBuzz.convert(3));
}

BDDでは「3の倍数が入力されたとき、“Fizz"と表示される」という振る舞いの仕様に焦点を当てます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# BDDスタイル:ビジネス的な視点
Feature: FizzBuzz Game
  As a player
  I want to know the FizzBuzz result
  So that I can play the game correctly

  Scenario: Number divisible by three
    Given I have the number 3
    When I check the FizzBuzz result
    Then I should see "Fizz"

Given-When-Then形式の理解

BDDの核心は、Given-When-Then形式によるシナリオ記述です。この形式は、テストを「前提条件」「アクション」「期待結果」の3つのパートで構造化します。

Given-When-Thenの役割

キーワード 役割 説明
Given 前提条件 システムの初期状態を設定(過去形で記述)
When アクション ユーザーやシステムが行う操作(現在形で記述)
Then 期待結果 アクション後に期待される結果(未来の結果として記述)
And / But 補助 同じ種類のステップを追加するための接続詞

効果的なシナリオの書き方

良いシナリオを書くためのベストプラクティスを見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 良い例:具体的で読みやすい
Feature: User Authentication
  Users should be able to securely log in to their accounts

  Scenario: Successful login with valid credentials
    Given a registered user with email "alice@example.com"
    And the user's password is "SecurePass123"
    When the user attempts to log in with email "alice@example.com" and password "SecurePass123"
    Then the user should be authenticated successfully
    And the user should be redirected to the dashboard

  Scenario: Failed login with incorrect password
    Given a registered user with email "bob@example.com"
    When the user attempts to log in with email "bob@example.com" and password "WrongPassword"
    Then the user should see an error message "Invalid email or password"
    And the user should remain on the login page

アンチパターンを避ける

シナリオ作成時に避けるべきアンチパターンも理解しておきましょう。

1
2
3
4
5
6
7
8
9
# 悪い例:実装詳細が露出している
Scenario: User login
  Given I open Chrome browser
  And I navigate to "https://example.com/login"
  And I find element with id "email-input"
  When I type "test@example.com" into the email field
  And I click the button with class "submit-btn"
  Then I should see HTTP status 200
  And the database should contain a session record
1
2
3
4
5
# 良い例:振る舞いに焦点を当てている
Scenario: User login
  Given I am on the login page
  When I log in with valid credentials
  Then I should be on the dashboard

Gherkin記法の基礎

Gherkinは、BDDシナリオを記述するための構造化言語です。人間が読める形式でありながら、自動テストツールが解釈できる規則を持っています。

Gherkinの主要キーワード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# language: ja
機能: ショッピングカート管理
  オンラインショップの顧客として
  商品をカートに追加・削除したい
  購入前に注文内容を確認できるように

  背景:
    前提 ログイン済みのユーザーである
    かつ カートが空である

  シナリオ: 商品をカートに追加
    前提 商品「プログラミング入門書」が在庫にある
    もし 「プログラミング入門書」をカートに追加する
    ならば カートに「プログラミング入門書」が1個入っている
    かつ カートの合計金額が表示される

  シナリオアウトライン: 複数商品の追加
    もし<商品名>」を<個数>個カートに追加する
    ならば カートに「<商品名>」が<個数>個入っている

    例:
      | 商品名           | 個数 |
      | JavaScript入門   | 2    |
      | React実践ガイド  | 1    |
      | TypeScript応用   | 3    |

英語版の構文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Feature: Shopping Cart Management
  As an online shop customer
  I want to add and remove items from my cart
  So that I can review my order before purchase

  Background:
    Given I am a logged-in user
    And my cart is empty

  Scenario: Add item to cart
    Given the product "Programming Guide" is in stock
    When I add "Programming Guide" to my cart
    Then my cart should contain 1 "Programming Guide"
    And the cart total should be displayed

  Scenario Outline: Add multiple items
    When I add <quantity> of "<product>" to my cart
    Then my cart should contain <quantity> of "<product>"

    Examples:
      | product              | quantity |
      | JavaScript Basics    | 2        |
      | React Practical Guide| 1        |
      | TypeScript Advanced  | 3        |

Data Tablesの活用

複雑なデータを扱う場合は、Data Tablesを使用します。

1
2
3
4
5
6
7
8
Scenario: Register multiple users
  Given the following users exist:
    | name    | email              | role    |
    | Alice   | alice@example.com  | admin   |
    | Bob     | bob@example.com    | user    |
    | Charlie | charlie@example.com| user    |
  When I view the user list
  Then I should see 3 users

JavaScriptでのBDD実装(JestとCucumber)

JavaScriptエコシステムでは、Jestをベースにしたjest-cucumberや、Cucumber.jsを使ってBDDを実践できます。

Jestでのbdd-lazyライブラリを使った実装

まず、シンプルにJestでBDDスタイルのテストを書く方法を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// shopping-cart.spec.js
describe('Shopping Cart', () => {
  describe('Feature: Add items to cart', () => {
    let cart
    let product

    describe('Scenario: Add a single item to empty cart', () => {
      // Given
      beforeEach(() => {
        cart = new ShoppingCart()
        product = { id: 1, name: 'JavaScript Guide', price: 2500 }
      })

      test('Given the cart is empty', () => {
        expect(cart.items).toHaveLength(0)
      })

      test('When I add a product to the cart', () => {
        cart.addItem(product)
      })

      test('Then the cart should contain 1 item', () => {
        cart.addItem(product)
        expect(cart.items).toHaveLength(1)
      })

      test('And the cart total should equal the product price', () => {
        cart.addItem(product)
        expect(cart.total).toBe(2500)
      })
    })
  })
})

Cucumber.jsによる本格的なBDD実装

Cucumber.jsを使用すると、Gherkinファイルとステップ定義を分離した本格的なBDD開発が可能です。

ディレクトリ構造:

project/
├── features/
│   ├── shopping-cart.feature
│   └── step-definitions/
│       └── shopping-cart.steps.js
├── src/
│   └── shopping-cart.js
├── package.json
└── cucumber.js

features/shopping-cart.feature:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Feature: Shopping Cart
  As a customer
  I want to manage items in my shopping cart
  So that I can purchase products I need

  Scenario: Add item to empty cart
    Given I have an empty shopping cart
    When I add a product "JavaScript Guide" priced at 2500 yen
    Then my cart should contain 1 item
    And the cart total should be 2500 yen

  Scenario: Add multiple quantities of same item
    Given I have an empty shopping cart
    When I add 3 of product "React Tutorial" priced at 3000 yen each
    Then my cart should contain 3 items
    And the cart total should be 9000 yen

  Scenario: Remove item from cart
    Given I have a cart with the following items:
      | name            | price | quantity |
      | JavaScript Guide| 2500  | 1        |
      | React Tutorial  | 3000  | 2        |
    When I remove "React Tutorial" from the cart
    Then my cart should contain 1 item
    And the cart total should be 2500 yen

features/step-definitions/shopping-cart.steps.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const { Given, When, Then } = require('@cucumber/cucumber')
const { expect } = require('chai')
const ShoppingCart = require('../../src/shopping-cart')

let cart

Given('I have an empty shopping cart', function () {
  cart = new ShoppingCart()
})

Given('I have a cart with the following items:', function (dataTable) {
  cart = new ShoppingCart()
  const items = dataTable.hashes()
  items.forEach(item => {
    const product = {
      name: item.name,
      price: parseInt(item.price)
    }
    for (let i = 0; i < parseInt(item.quantity); i++) {
      cart.addItem(product)
    }
  })
})

When('I add a product {string} priced at {int} yen', function (name, price) {
  cart.addItem({ name, price })
})

When('I add {int} of product {string} priced at {int} yen each', function (quantity, name, price) {
  for (let i = 0; i < quantity; i++) {
    cart.addItem({ name, price })
  }
})

When('I remove {string} from the cart', function (productName) {
  cart.removeItem(productName)
})

Then('my cart should contain {int} item(s)', function (expectedCount) {
  expect(cart.itemCount).to.equal(expectedCount)
})

Then('the cart total should be {int} yen', function (expectedTotal) {
  expect(cart.total).to.equal(expectedTotal)
})

src/shopping-cart.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ShoppingCart {
  constructor() {
    this.items = []
  }

  addItem(product) {
    this.items.push(product)
  }

  removeItem(productName) {
    this.items = this.items.filter(item => item.name !== productName)
  }

  get itemCount() {
    return this.items.length
  }

  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0)
  }
}

module.exports = ShoppingCart

JavaでのBDD実装(JUnitとCucumber)

Javaエコシステムでは、Cucumber-JVMを使用してBDDを実践できます。

プロジェクト構成

pom.xml(依存関係):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependencies>
    <!-- Cucumber -->
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-java</artifactId>
        <version>7.18.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.cucumber</groupId>
        <artifactId>cucumber-junit-platform-engine</artifactId>
        <version>7.18.0</version>
        <scope>test</scope>
    </dependency>
    
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-suite</artifactId>
        <version>1.10.3</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Assertions -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.26.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

ディレクトリ構造:

src/
├── main/java/com/example/
│   └── ShoppingCart.java
└── test/
    ├── java/com/example/
    │   ├── RunCucumberTest.java
    │   └── steps/
    │       └── ShoppingCartSteps.java
    └── resources/features/
        └── shopping-cart.feature

Featureファイル

src/test/resources/features/shopping-cart.feature:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Feature: Shopping Cart Management
  As an online shop customer
  I want to manage my shopping cart
  So that I can purchase the products I need

  Background:
    Given the following products are available:
      | id  | name             | price |
      | 101 | Java入門         | 3500  |
      | 102 | Spring Boot実践  | 4200  |
      | 103 | JUnit完全ガイド  | 2800  |

  Scenario: Add single item to empty cart
    Given I have an empty cart
    When I add product with id 101 to my cart
    Then my cart should have 1 item
    And the cart total should be 3500 yen

  Scenario: Calculate total for multiple items
    Given I have an empty cart
    When I add product with id 101 to my cart
    And I add product with id 102 to my cart
    Then my cart should have 2 items
    And the cart total should be 7700 yen

  Scenario Outline: Apply discount based on total
    Given I have items totaling <subtotal> yen in my cart
    When I apply the discount rules
    Then the discount should be <discount> percent
    And the final total should be <final> yen

    Examples:
      | subtotal | discount | final |
      | 5000     | 0        | 5000  |
      | 10000    | 5        | 9500  |
      | 20000    | 10       | 18000 |

ステップ定義

src/test/java/com/example/steps/ShoppingCartSteps.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.example.steps;

import com.example.Product;
import com.example.ShoppingCart;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.When;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.And;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

public class ShoppingCartSteps {

    private Map<Integer, Product> availableProducts = new HashMap<>();
    private ShoppingCart cart;
    private int appliedDiscount;

    @Given("the following products are available:")
    public void theFollowingProductsAreAvailable(DataTable dataTable) {
        List<Map<String, String>> rows = dataTable.asMaps();
        for (Map<String, String> row : rows) {
            int id = Integer.parseInt(row.get("id"));
            String name = row.get("name");
            int price = Integer.parseInt(row.get("price"));
            availableProducts.put(id, new Product(id, name, price));
        }
    }

    @Given("I have an empty cart")
    public void iHaveAnEmptyCart() {
        cart = new ShoppingCart();
    }

    @Given("I have items totaling {int} yen in my cart")
    public void iHaveItemsTotalingYenInMyCart(int total) {
        cart = new ShoppingCart();
        // ダミー商品を追加して合計金額を設定
        cart.addItem(new Product(999, "Dummy", total));
    }

    @When("I add product with id {int} to my cart")
    public void iAddProductWithIdToMyCart(int productId) {
        Product product = availableProducts.get(productId);
        cart.addItem(product);
    }

    @When("I apply the discount rules")
    public void iApplyTheDiscountRules() {
        appliedDiscount = cart.calculateDiscountPercent();
    }

    @Then("my cart should have {int} item(s)")
    public void myCartShouldHaveItems(int expectedCount) {
        assertThat(cart.getItemCount()).isEqualTo(expectedCount);
    }

    @Then("the cart total should be {int} yen")
    public void theCartTotalShouldBeYen(int expectedTotal) {
        assertThat(cart.getSubtotal()).isEqualTo(expectedTotal);
    }

    @Then("the discount should be {int} percent")
    public void theDiscountShouldBePercent(int expectedDiscount) {
        assertThat(appliedDiscount).isEqualTo(expectedDiscount);
    }

    @And("the final total should be {int} yen")
    public void theFinalTotalShouldBeYen(int expectedFinal) {
        int finalTotal = cart.calculateFinalTotal();
        assertThat(finalTotal).isEqualTo(expectedFinal);
    }
}

テストランナー

src/test/java/com/example/RunCucumberTest.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example;

import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectClasspathResource;
import org.junit.platform.suite.api.Suite;

import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;

@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("features")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.steps")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, html:target/cucumber-reports.html")
public class RunCucumberTest {
}

BDDとTDDを組み合わせた開発フロー

BDDとTDDは対立する概念ではなく、補完的な関係にあります。両者を組み合わせることで、より効果的な開発フローを構築できます。

Double Loop TDD

Outside-In TDDとも呼ばれるこのアプローチでは、外側のBDDループと内側のTDDループを組み合わせます。

graph TB
    subgraph "外側のループ(BDD)"
        A[受け入れテスト作成<br/>Red] --> B[機能実装<br/>Green]
        B --> C[シナリオ完了<br/>Refactor]
        C --> A
    end
    
    subgraph "内側のループ(TDD)"
        D[ユニットテスト作成<br/>Red] --> E[最小実装<br/>Green]
        E --> F[リファクタリング]
        F --> D
    end
    
    B -.-> D
    F -.-> B
    
    style A fill:#ffcccc,stroke:#cc0000,color:#000000
    style B fill:#ccffcc,stroke:#00cc00,color:#000000
    style C fill:#cce5ff,stroke:#0066cc,color:#000000
    style D fill:#ffcccc,stroke:#cc0000,color:#000000
    style E fill:#ccffcc,stroke:#00cc00,color:#000000
    style F fill:#cce5ff,stroke:#0066cc,color:#000000

実践的なワークフロー

  1. ユーザーストーリーの作成: ビジネス要件をユーザーストーリーとして定義
  2. Discoveryセッション: ステークホルダーと具体例を探索
  3. シナリオの形式化: Gherkin形式でシナリオを記述(BDD)
  4. 受け入れテストの作成: シナリオを実行可能なテストに変換
  5. TDDで内部実装: Red-Green-Refactorサイクルでコンポーネントを実装
  6. 受け入れテストがパス: 外側のループが完了
  7. 次のシナリオへ: 繰り返し

具体例:ユーザー登録機能

Step 1: BDDシナリオ作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Feature: User Registration
  As a new user
  I want to register an account
  So that I can access the application

  Scenario: Successful registration with valid data
    Given I am on the registration page
    When I register with the following details:
      | email            | password      | name    |
      | test@example.com | SecurePass123 | Taro    |
    Then my account should be created
    And I should receive a welcome email
    And I should be logged in automatically

Step 2: 受け入れテスト作成(Red状態)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Given("I am on the registration page")
public void iAmOnTheRegistrationPage() {
    registrationPage = new RegistrationPage(driver);
    registrationPage.navigate();
}

@When("I register with the following details:")
public void iRegisterWithTheFollowingDetails(DataTable dataTable) {
    Map<String, String> data = dataTable.asMaps().get(0);
    registrationPage.fillForm(
        data.get("email"),
        data.get("password"),
        data.get("name")
    );
    registrationPage.submit();
}

@Then("my account should be created")
public void myAccountShouldBeCreated() {
    // この時点ではUserServiceが存在しないため失敗
    assertThat(userService.findByEmail("test@example.com")).isPresent();
}

Step 3: 内側のTDDループでUserServiceを実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// TDDサイクル1: ユーザー保存機能
@Test
void shouldSaveNewUser() {
    // Red: テストを書く
    UserService service = new UserService(userRepository);
    User user = new User("test@example.com", "SecurePass123", "Taro");
    
    service.register(user);
    
    verify(userRepository).save(user);
}

// TDDサイクル2: メール送信機能
@Test
void shouldSendWelcomeEmailAfterRegistration() {
    UserService service = new UserService(userRepository, emailService);
    User user = new User("test@example.com", "SecurePass123", "Taro");
    
    service.register(user);
    
    verify(emailService).sendWelcomeEmail("test@example.com", "Taro");
}

// TDDサイクル3: 重複チェック
@Test
void shouldRejectDuplicateEmail() {
    when(userRepository.existsByEmail("existing@example.com")).thenReturn(true);
    UserService service = new UserService(userRepository, emailService);
    User user = new User("existing@example.com", "Pass123", "User");
    
    assertThrows(DuplicateEmailException.class, () -> service.register(user));
}

Step 4: 受け入れテストがパス(Green状態)

すべてのユニットテストがパスし、コンポーネントが完成したら、受け入れテストもパスします。

BDDを導入すべきシーンと避けるべきシーン

BDDはすべてのプロジェクトに適しているわけではありません。導入の判断基準を理解しましょう。

BDDが効果的なシーン

シーン 理由
複雑なビジネスロジック 仕様の明文化により、認識のずれを防げる
ステークホルダーとの協業が多い 非エンジニアにも読めるドキュメントになる
長期間メンテナンスするシステム 実行可能な仕様書として価値が持続する
アジャイル開発チーム ユーザーストーリーとの親和性が高い
規制が厳しい業界(金融・医療等) 監査可能なドキュメントとして機能する

BDDが向かないシーン

シーン 理由
プロトタイプ開発 仕様が頻繁に変わりオーバーヘッドになる
技術的なライブラリ開発 ユーザーストーリーより技術仕様が重要
小規模な個人プロジェクト コミュニケーションコストがそもそも低い
非常にシンプルなCRUDアプリ 仕様書なしでも明確なケース

BDDツールエコシステム

主要なBDDツールを把握し、プロジェクトに適したものを選択しましょう。

言語別BDDツール比較

言語/環境 ツール 特徴
JavaScript Cucumber.js Gherkin完全サポート、Node.js環境で動作
JavaScript jest-cucumber JestとCucumberの統合
Java Cucumber-JVM JUnit統合、豊富なレポート機能
Java JBehave Story形式のBDDフレームワーク
Python Behave Gherkinサポート、pytest統合
Ruby Cucumber BDDの元祖、Rubyネイティブ
.NET SpecFlow Visual Studio統合、C#/VB対応
Go Godog Cucumber公式のGo実装

レポート機能

Cucumberは様々なフォーマットでレポートを出力できます。

1
2
3
4
5
6
7
8
# HTML形式レポート
cucumber-js --format html:reports/cucumber-report.html

# JSON形式レポート(CI/CD連携用)
cucumber-js --format json:reports/cucumber-report.json

# JUnit XML形式(CI/CDツール連携)
cucumber-js --format junit:reports/cucumber-junit.xml

まとめ

BDDとTDDは、それぞれ異なる視点からソフトウェア開発を支援するアプローチです。

TDDの焦点:

  • 技術的な正しさの検証
  • Red-Green-Refactorによる設計改善
  • 開発者間のコミュニケーション

BDDの焦点:

  • ビジネス価値の明確化
  • ステークホルダーとの共通言語構築
  • 実行可能な仕様書としてのテスト

両者を組み合わせたDouble Loop TDDを実践することで、以下のメリットが得られます。

  1. ビジネス要件と実装の一貫性確保
  2. 非エンジニアにも理解可能なドキュメント
  3. 堅牢なリグレッションテストスイート
  4. 仕様変更への迅速な対応

BDDは単なるテスト技法ではなく、チーム全体のコミュニケーションを改善するためのプラクティスです。Gherkin記法やCucumberといったツールは、その目的を達成するための手段に過ぎません。重要なのは、ビジネスと技術の間のギャップを埋め、顧客に価値を届け続けることです。

参考リンク