BDD(振る舞い駆動開発)を導入しようとして、「シナリオが書けない」「ステップ定義の量が膨大になる」「エンジニアしか触らなくなった」という壁にぶつかったことはないでしょうか。
私自身、複数のプロジェクトで BDD を試みては挫折した経験があります。Gherkin の文法は覚えられても、「ビジネス価値を反映したシナリオ」を一から書くのは思いのほか難しく、メンテナンスコストが見合わないと感じていました。
ところが Claude Code を本格的に使い始めてから、BDD への見方が変わりました。シナリオ生成・ステップ定義・テストコードの自動化を Claude Code に任せると、BDD が「書くもの」から「育てるもの」に変わるのです。ここで扱うのはその具体的な実装方法を動作確認済みのコードとともにお伝えします。
BDD の概念を整理しつつ、そして今なぜ Claude Code と組み合わせるのか
BDD(Behavior-Driven Development)は、アプリケーションの「振る舞い」を自然言語で記述し、それをテストコードの仕様として活用する開発手法です。TDD がコードの正しさを検証するのに対し、BDD はビジネスの意図を記述します。
Gherkin という DSL(ドメイン固有言語)を使い、こんな形でシナリオを書きます:
Feature : ユーザーログイン機能
背景にある価値: 認証されたユーザーのみがダッシュボードにアクセスできる
Scenario : 正しい認証情報でログインが成功する
Given ユーザーが登録済みアカウントを持っている
When メールアドレス "test@example.com" とパスワード "SecurePass123" を入力する
And ログインボタンをクリックする
Then ダッシュボードページに遷移する
And 「ようこそ、テストユーザーさん」というメッセージが表示される
Scenario : 誤ったパスワードでログインが失敗する
Given ユーザーが登録済みアカウントを持っている
When メールアドレス "test@example.com" と誤ったパスワード "WrongPass" を入力する
And ログインボタンをクリックする
Then エラーメッセージ「メールアドレスまたはパスワードが正しくありません」が表示される
And ダッシュボードには遷移しない
このシナリオを手で書くのは、機能が複雑になるにつれて現実的ではなくなります。Claude Code を使うと、要件定義書やユーザーストーリーからシナリオを自動生成し、さらにステップ定義まで自動化できます。
プロジェクトのセットアップ
必要なパッケージのインストール
まず、Next.js プロジェクトに BDD 環境を整えます:
# Playwright + Cucumber.js の組み合わせで BDD 環境を構築
npm install --save-dev \
@cucumber/cucumber \
@playwright/test \
playwright \
@types/node
# Playwright ブラウザのインストール
npx playwright install chromium
プロジェクト構造はこのように整理します:
project-root/
├── features/ # Gherkin シナリオファイル
│ ├── auth/
│ │ └── login.feature
│ ├── dashboard/
│ │ └── overview.feature
│ └── support/
│ └── world.ts # Cucumber の World 設定
├── steps/ # ステップ定義
│ ├── auth/
│ │ └── login.steps.ts
│ └── common/
│ └── navigation.steps.ts
└── cucumber.config.ts # Cucumber 設定ファイル
cucumber.config.ts の基本設定:
// cucumber.config.ts
import { defineConfig } from '@cucumber/cucumber' ;
export default defineConfig ({
default: {
requireModule: [ 'ts-node/register' ],
require: [ 'steps/**/*.ts' , 'features/support/**/*.ts' ],
format: [
'progress-bar' ,
'json:reports/cucumber-report.json' ,
'html:reports/cucumber-report.html'
],
formatOptions: { snippetInterface: 'async-await' },
worldParameters: {
baseUrl: process.env. BASE_URL || 'http://localhost:3000' ,
},
timeout: 30000 ,
retry: 1 ,
} ,
}) ;
Claude Code で Gherkin シナリオを自動生成する
ここが Claude Code の真価が発揮される部分です。要件定義書やユーザーストーリーを Claude Code に渡すと、BDD に適したシナリオを生成してくれます。
CLAUDE.md に BDD ガイドラインを記述する
<!-- .claude/CLAUDE.md -->
## BDD シナリオ生成ガイドライン
新機能を実装するとき、または既存機能を変更するときは、
必ず以下の手順で BDD シナリオを先に作成すること。
### Gherkin 記述ルール
- Feature の説明には「背景にある価値」を1文で記述する
- Scenario タイトルは「条件」から「結果」が明確になるように書く
- Given/When/Then は1行ずつ、1つの動作のみを記述する
- データテーブルや Examples を活用してシナリオを簡潔にする
- ビジネス用語を使い、技術的な実装詳細は含めない
### 生成コマンド
- `bdd generate <機能名>` でシナリオを生成
- `bdd steps <feature ファイル>` でステップ定義を生成
- `bdd review` で既存シナリオの品質レビュー
Claude Code でシナリオを生成するセッション例:
# Claude Code の対話例
You: ECサイトのカート機能のBDDシナリオを生成してください。
要件:商品の追加・削除・数量変更・合計金額計算が必要です。
Claude Code: [features/cart/cart.feature を生成]
Feature: ショッピングカート機能
背景にある価値: ユーザーが購入したい商品を一時的に保管し、合計金額を確認できる
Background:
Given ユーザーがログイン済みである
And 商品 "ワイヤレスイヤホン Pro" が在庫 10 個で価格 12800 円で登録されている
And 商品 "USBケーブル" が在庫 50 個で価格 880 円で登録されている
Scenario: カートに商品を追加する
When 商品 "ワイヤレスイヤホン Pro" の商品ページを開く
And 「カートに追加」ボタンをクリックする
Then カートのアイコンに数字 "1" が表示される
And カートページを開くと "ワイヤレスイヤホン Pro" が1個表示される
And カートの合計金額が 12800 円と表示される
Scenario: カートの商品数量を変更する
Given カートに "ワイヤレスイヤホン Pro" が1個入っている
When 数量を "3" に変更する
Then カートの合計金額が 38400 円と表示される
Scenario Outline: 複数の商品をカートに追加する
Given カートが空である
When 以下の商品をカートに追加する:
| 商品名 | 数量 |
| ワイヤレスイヤホン Pro | <qty1> |
| USBケーブル | <qty2> |
Then カートの合計金額が <expected> 円と表示される
Examples:
| qty1 | qty2 | expected |
| 1 | 1 | 13680 |
| 2 | 3 | 28240 |
| 0 | 5 | 4400 |
Scenario: 在庫不足の場合にエラーが表示される
Given カートに "ワイヤレスイヤホン Pro" が 9 個入っている
When 数量を "11" に変更しようとする
Then エラーメッセージ「在庫数を超えて追加することはできません(在庫: 10個)」が表示される
And カートの数量は 9 個のまま変わらない
このようなシナリオを手動で網羅的に書くのは困難ですが、Claude Code に任せると見落としがちなエッジケース(在庫不足、数量0など)も自然に含まれるようになります。
ステップ定義の自動生成
シナリオができたら、次はステップ定義です。Claude Code に .feature ファイルを渡すと、TypeScript のステップ定義を自動生成できます。
// steps/cart/cart.steps.ts
// Claude Code が自動生成したステップ定義(エラーハンドリング付き)
import { Given, When, Then } from '@cucumber/cucumber' ;
import { expect } from '@playwright/test' ;
import type { CustomWorld } from '../support/world' ;
Given ( 'カートが空である' , async function ( this : CustomWorld ) {
// カートをクリアするAPIを呼び出す
const response = await this .page.request. post ( '/api/cart/clear' , {
headers: { Authorization: `Bearer ${ this . authToken }` }
});
if ( ! response. ok ()) {
throw new Error ( `カートのクリアに失敗: ${ response . status () } ${ await response . text () }` );
}
// カートページを開いて空であることを確認
await this .page. goto ( '/cart' );
await expect ( this .page. getByTestId ( 'cart-empty-message' )). toBeVisible ();
});
When ( '商品 {string} の商品ページを開く' , async function ( this : CustomWorld , productName : string ) {
// 商品名から商品ページURLを検索して遷移
await this .page. goto ( '/products' );
const productLink = this .page. getByRole ( 'link' , { name: productName });
await expect (productLink). toBeVisible ({ timeout: 5000 });
await productLink. click ();
// ページ遷移完了を待機
await this .page. waitForLoadState ( 'networkidle' );
this .currentProductName = productName;
});
When ( '「カートに追加」ボタンをクリックする' , async function ( this : CustomWorld ) {
const addToCartButton = this .page. getByRole ( 'button' , { name: 'カートに追加' });
await expect (addToCartButton). toBeEnabled ({ timeout: 3000 });
await addToCartButton. click ();
// 追加完了の確認(トースト通知や数量更新を待つ)
await this .page. waitForResponse (
response => response. url (). includes ( '/api/cart' ) && response. status () === 200 ,
{ timeout: 5000 }
);
});
Then ( 'カートのアイコンに数字 {string} が表示される' , async function ( this : CustomWorld , count : string ) {
const cartBadge = this .page. getByTestId ( 'cart-badge' );
await expect (cartBadge). toBeVisible ();
await expect (cartBadge). toHaveText (count);
});
When ( '以下の商品をカートに追加する:' , async function ( this : CustomWorld , dataTable : any ) {
const items = dataTable. hashes ();
for ( const item of items) {
const qty = parseInt (item[ '数量' ]);
if (qty === 0 ) continue ; // 数量が0の場合はスキップ
// 商品ページへ遷移
await this .page. goto ( '/products' );
const productLink = this .page. getByRole ( 'link' , { name: item[ '商品名' ] });
await productLink. click ();
await this .page. waitForLoadState ( 'networkidle' );
// 数量を設定
if (qty > 1 ) {
const qtyInput = this .page. getByRole ( 'spinbutton' , { name: '数量' });
await qtyInput. fill ( String (qty));
}
// カートに追加
await this .page. getByRole ( 'button' , { name: 'カートに追加' }). click ();
await this .page. waitForResponse (
response => response. url (). includes ( '/api/cart' ) && response. status () === 200 ,
{ timeout: 5000 }
);
}
});
Then ( 'カートの合計金額が {int} 円と表示される' , async function ( this : CustomWorld , expectedPrice : number ) {
await this .page. goto ( '/cart' );
const totalElement = this .page. getByTestId ( 'cart-total-price' );
await expect (totalElement). toBeVisible ();
// 表示価格の数値を抽出して比較
const priceText = await totalElement. textContent ();
const actualPrice = parseInt (priceText?. replace ( / [ ^ 0-9] / g , '' ) || '0' );
expect (actualPrice). toBe (expectedPrice);
});
ステップ定義でよく見落とされるのがエラーハンドリング です。Claude Code に生成を任せると、waitForResponse・タイムアウト設定・エラーメッセージの明示など、本番で実際に必要になるコードが自然に含まれます。私が手で書くと省略しがちな部分を、Claude Code は忠実に実装してくれます。
World オブジェクトの設計
Cucumber の World は、ステップ間で状態を共有するための仕組みです。適切に設計すると、テストの可読性と再利用性が大幅に向上します。
// features/support/world.ts
import { World, IWorldOptions, setWorldConstructor } from '@cucumber/cucumber' ;
import { Browser, BrowserContext, Page, chromium } from '@playwright/test' ;
export interface CustomWorld extends World {
browser : Browser ;
context : BrowserContext ;
page : Page ;
authToken : string ;
currentProductName ?: string ;
testData : Record < string , any >;
}
class PlaywrightWorld extends World implements CustomWorld {
browser !: Browser ;
context !: BrowserContext ;
page !: Page ;
authToken : string = '' ;
currentProductName ?: string ;
testData : Record < string , any > = {};
constructor ( options : IWorldOptions ) {
super (options);
}
}
// フック設定(Before/After)
import { Before, After, BeforeAll, AfterAll } from '@cucumber/cucumber' ;
let sharedBrowser : Browser ;
BeforeAll ( async function () {
sharedBrowser = await chromium. launch ({
headless: process.env. HEADED !== 'true' ,
slowMo: process.env. SLOW_MO ? parseInt (process.env. SLOW_MO ) : 0 ,
});
});
AfterAll ( async function () {
await sharedBrowser?. close ();
});
Before ( async function ( this : CustomWorld ) {
// テストごとに独立したコンテキストを作成(Cookie・ストレージが隔離される)
this .context = await sharedBrowser. newContext ({
baseURL: process.env. BASE_URL || 'http://localhost:3000' ,
viewport: { width: 1280 , height: 720 },
locale: 'ja-JP' ,
});
this .page = await this .context. newPage ();
// コンソールエラーをテストログに記録
this .page. on ( 'console' , ( msg ) => {
if (msg. type () === 'error' ) {
console. error ( `Browser Console Error: ${ msg . text () }` );
}
});
});
After ( async function ( this : CustomWorld , scenario ) {
// テスト失敗時のスクリーンショット保存
if (scenario.result?.status === 'FAILED' ) {
const screenshotPath = `reports/screenshots/${ scenario . pickle . name . replace ( / \s / g , '_' ) }.png` ;
await this .page. screenshot ({ path: screenshotPath, fullPage: true });
this . attach ( await this .page. screenshot (), 'image/png' );
}
await this .context?. close ();
});
setWorldConstructor (PlaywrightWorld);
CI/CD パイプラインへの統合
BDD テストを GitHub Actions で自動実行する設定です:
# .github/workflows/bdd-tests.yml
name : BDD Tests
on :
push :
branches : [ main , develop ]
pull_request :
branches : [ main ]
jobs :
bdd-test :
runs-on : ubuntu-latest
services :
postgres :
image : postgres:16
env :
POSTGRES_PASSWORD : testpassword
POSTGRES_DB : test_db
options : >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps :
- uses : actions/checkout@v4
- name : Setup Node.js
uses : actions/setup-node@v4
with :
node-version : '22'
cache : 'npm'
- name : Install dependencies
run : npm ci
- name : Install Playwright browsers
run : npx playwright install chromium
- name : Build application
run : npm run build
env :
DATABASE_URL : postgresql://postgres:testpassword@localhost:5432/test_db
- name : Run database migrations
run : npm run db:migrate
env :
DATABASE_URL : postgresql://postgres:testpassword@localhost:5432/test_db
- name : Start application
run : npm start &
env :
DATABASE_URL : postgresql://postgres:testpassword@localhost:5432/test_db
PORT : 3000
- name : Wait for application startup
run : npx wait-on http://localhost:3000 --timeout 30000
- name : Run BDD tests
run : npx cucumber-js
env :
BASE_URL : http://localhost:3000
- name : Upload test reports
uses : actions/upload-artifact@v4
if : always()
with :
name : cucumber-reports
path : reports/
retention-days : 14
- name : Upload failure screenshots
uses : actions/upload-artifact@v4
if : failure()
with :
name : failure-screenshots
path : reports/screenshots/
PR レビューに BDD シナリオを含める
Claude Code を使った開発フローでは、新機能の実装と並行して BDD シナリオの更新も自動化できます。私がよく使うパターンは、PR の説明文にシナリオを添付することです:
# .claude/commands/bdd-pr.md
# PR作成前にBDDシナリオを確認・更新するコマンド
1. 変更された機能に関連する .feature ファイルを特定する
2. 既存シナリオが変更後の仕様を反映しているか確認する
3. 不足しているシナリオを追加生成する
4. ステップ定義を更新する
5. テストをローカルで実行して通過を確認する
6. PR の説明文に追加・変更したシナリオを記載する
よくある落とし穴と対処法
BDD を導入して躓くポイントを3つ挙げます。
1. シナリオが技術的になりすぎる
# ❌ 技術的すぎるシナリオ(避けるべき)
When ユーザーが POST /api/auth/login にリクエストを送信する
And レスポンスが 200 OK で JWT トークンを返す
# ✅ ビジネス観点のシナリオ
When ユーザーが正しいメールアドレスとパスワードを入力してログインする
Then ダッシュボードが表示される
Claude Code は「ビジネス観点のシナリオで書いてください」と明示的に指示しないと、実装寄りのシナリオを生成することがあります。CLAUDE.md に記述ルールを明確に書いておく点が肝心です。
2. Background が膨らみすぎる
Background セクションに10行以上の前提条件を詰め込むと、シナリオの可読性が著しく下がります。Claude Code に「Background を3行以内にリファクタリングしてください」と依頼すると、テストデータをフィクスチャーに切り出す提案をしてくれます。
3. ステップの粒度が揃わない
「ログインする」という操作を、あるシナリオでは1ステップ、別のシナリオでは3ステップで書いてしまうと、メンテナンスが大変になります。共通ステップを common/ フォルダに整理し、Claude Code に「このシナリオの共通化できる部分を指摘してください」と定期的にレビューさせることが効果的です。
4. フレーキーテスト(不安定なテスト)への対処
非同期処理の多い現代の Web アプリでは、タイミング依存のテスト失敗が頻発します。対処法として Claude Code が提案するパターンを紹介します:
// ❌ フレーキーになりやすい実装
await this .page. click ( '[data-testid="submit-button"]' );
await this .page. waitForTimeout ( 1000 ); // 固定待機は禁物
// ✅ イベント駆動の待機
await Promise . all ([
this .page. waitForResponse (
response => response. url (). includes ( '/api/' ) && response. status () < 400
),
this .page. click ( '[data-testid="submit-button"]' )
]);
// ✅ ネットワークアイドル状態の待機
await this .page. click ( '[data-testid="submit-button"]' );
await this .page. waitForLoadState ( 'networkidle' , { timeout: 10000 });
非エンジニアも参加できるシナリオ設計
BDD の本来の目的は、エンジニアとビジネス側が同じ「シナリオ」を共有言語として使うことです。Claude Code を活用した非エンジニア向けのワークフローを紹介します。
PO・企画担当者向けのシナリオ作成フロー
要件を箇条書きで書いてもらう (日本語の自然文でよい)
Claude Code で Gherkin に変換する
PO に確認・修正してもらう (技術的な部分は触らなくてよい)
エンジニアがステップ定義を実装する
# PO が書いた要件(自然文)
「ユーザーがプロフィール写真を変更できるようにしてほしい。
JPEG と PNG のみ対応。5MB 以下。変更後は即座に反映される。」
# Claude Code が変換した Gherkin
Feature: プロフィール写真の変更
Scenario: 有効な画像ファイルでプロフィール写真を変更する
Given ユーザーがプロフィール設定ページを開いている
When JPEG 形式 2MB のファイルをアップロードする
Then プロフィール写真が新しい画像に更新される
And 変更がヘッダーのアイコンにも即座に反映される
Scenario: 5MB を超える画像ファイルはアップロードできない
Given ユーザーがプロフィール設定ページを開いている
When 6MB の PNG ファイルをアップロードしようとする
Then エラーメッセージ「ファイルサイズは 5MB 以下にしてください」が表示される
And プロフィール写真は変更されない
Scenario: サポートされていないファイル形式はアップロードできない
Given ユーザーがプロフィール設定ページを開いている
When GIF 形式のファイルをアップロードしようとする
Then エラーメッセージ「JPEG または PNG 形式のファイルを選択してください」が表示される
このフローで重要なのは、PO がシナリオを「読めて・修正できる」状態を維持する ことです。実装の詳細をシナリオに混ぜないように Claude Code に指示しておくことで、非エンジニアが内容を把握しやすくなります。
シナリオの品質レビューを Claude Code に依頼する
定期的に既存シナリオの品質チェックを行うことで、陳腐化や冗長化を防げます:
# Claude Code へのレビュー依頼コマンド例
# .claude/commands/bdd-review.md
## BDD シナリオ品質レビュー
以下の観点で features/ ディレクトリ全体をレビューしてください:
1. シナリオが技術的な実装詳細を含んでいないか
2. Background に過剰な前提条件が詰め込まれていないか
3. 同じステップが複数のシナリオで繰り返されていないか(共通化の余地)
4. エッジケース(境界値・エラーケース)が不足していないか
5. シナリオ名が「条件 → 結果」の形式で書かれているか
6. 1つのシナリオが複数の振る舞いを検証していないか(単一責任)
改善提案はシナリオの書き換え案とともに提示してください。
本番運用での BDD 継続のコツ
BDD を組み込んだ後、長期的に継続させるためのポイントをいくつか共有します。
テスト実行時間の管理 : シナリオが増えると実行時間が長くなります。@smoke @regression @critical のようなタグで分類し、PR ごとの実行は smoke のみ、定期実行でフル実行する運用が実用的です。
@smoke @auth
Scenario : 正しい認証情報でログインが成功する
...
@regression @cart
Scenario : 在庫不足の場合にエラーが表示される
...
シナリオの「負債」を溜めない : 機能変更時にシナリオの更新を後回しにすると、すぐに現実と乖離します。PR のマージ条件に「関連するシナリオの更新」を含めることで、シナリオの鮮度を保てます。
パフォーマンス目標の設定 : 私のプロジェクトでは、smoke テストは 3 分以内・フル regression は 20 分以内という目標を設けています。それを超えそうになった時点で、並列実行の設定を見直すシグナルとして扱います。
全体を振り返って:最初の一歩
BDD は「仕組みを作って終わり」ではなく、チームの中で育てていくものです。まず試してほしいのは、今取り組んでいる機能の主要シナリオを1つ書くこと です。Claude Code に「この機能のシナリオを Gherkin で書いてください」と依頼するだけで、十分な質のシナリオが生成されます。
ステップ定義の自動生成・CI 統合は後から追加できます。最初の一歩は「シナリオが存在する状態を作ること」です。5 分あれば始められるので、ぜひ今日のプロジェクトで試してみてください。
BDD のアプローチについてより体系的に