BDD事始め:Gherkin+Cucumber+PlayWrightで作るものの解像度を初動でMAXに
注意:この記事はAIが作成しています
参照元の存在、参考元のリンクの信頼度、事実の歪曲がないかをAIによりセルフチェックしています
はじめに
プロジェクトの初期段階で「何を作るのか」の認識がチーム内でズレていることはありませんか?BDD(Behavior-Driven Development:振る舞い駆動開発)は、この課題を解決し、開発初期から要求仕様の解像度を最大化する強力なアプローチです。
本記事では、Gherkin記法によるシナリオ記述、Cucumberによる自動化、そしてPlayWrightを使ったE2Eテストの実装を組み合わせることで、プロダクトの振る舞いを明確に定義し、チーム全体で共通認識を持つ方法を解説します。
BDDとは何か
BDDは、TDD(Test-Driven Development)から派生した開発手法で、システムの「振る舞い」に焦点を当てます。技術的な実装詳細ではなく、ユーザーから見たシステムの動作を中心に据えることで、ビジネス側と開発側の認識のギャップを埋めます。
BDDの3つの核心要素
Gherkin記法:全員が理解できる仕様書
Gherkinは、自然言語に近い形式でテストシナリオを記述する記法です。プログラマーでなくても理解でき、かつ実行可能なテストとして機能します。
基本構文
Feature: ショッピングカートの管理
ユーザーとして
商品をカートに追加・削除できる
購入前に商品を管理したいから
Background:
Given ユーザーがECサイトにログインしている
And 商品一覧ページを表示している
Scenario: 商品をカートに追加する
Given 商品「プログラミング入門書」が表示されている
When ユーザーが「カートに追加」ボタンをクリックする
Then カートに「プログラミング入門書」が1冊追加される
And カートアイコンに「1」と表示される
Scenario Outline: 複数商品の追加
When ユーザーが<商品名>を<個数>個カートに追加する
Then カートの合計金額は<合計金額>円になる
Examples:
| 商品名 | 個数 | 合計金額 |
| ノートPC | 1 | 150000 |
| マウス | 2 | 6000 |
| キーボード | 1 | 12000 |
Gherkinのキーワード解説
キーワード | 目的 | 使用タイミング |
---|---|---|
Feature | 機能全体の説明 | ファイルの冒頭に1つ |
Background | 共通の前提条件 | 各シナリオ実行前に毎回実行 |
Scenario | 具体的なテストケース | 1つの振る舞いを検証 |
Given | 前提条件 | テストの初期状態を設定 |
When | アクション | ユーザーの操作を表現 |
Then | 期待結果 | 検証すべき結果を記述 |
And/But | 接続詞 | 複数の条件やアクションを連結 |
Cucumber:シナリオを実行可能にする
Cucumberは、Gherkinで書かれたシナリオを実際のテストコードに変換するツールです。JavaScript/TypeScriptの実装例を見てみましょう。
プロジェクトセットアップ
# パッケージのインストール
npm init -y
npm install --save-dev @cucumber/cucumber playwright @playwright/test typescript ts-node
npm install --save-dev @types/node
# PlayWrightのブラウザをインストール
npx playwright install
ディレクトリ構造
project-root/
├── features/
│ ├── shopping-cart.feature
│ └── step-definitions/
│ └── shopping-cart.steps.ts
├── support/
│ ├── world.ts
│ └── hooks.ts
├── cucumber.js
├── tsconfig.json
└── package.json
Step Definitionsの実装
// features/step-definitions/shopping-cart.steps.ts
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from '@playwright/test';
import { CustomWorld } from '../../support/world';
Given('ユーザーがECサイトにログインしている', async function (this: CustomWorld) {
await this.page.goto('https://example-shop.com');
await this.page.fill('#email', 'test@example.com');
await this.page.fill('#password', 'password123');
await this.page.click('button[type="submit"]');
await this.page.waitForSelector('.user-dashboard');
});
Given('商品一覧ページを表示している', async function (this: CustomWorld) {
await this.page.goto('https://example-shop.com/products');
await this.page.waitForSelector('.product-list');
});
Given('商品{string}が表示されている', async function (this: CustomWorld, productName: string) {
const product = await this.page.locator(`.product-card:has-text("${productName}")`);
await expect(product).toBeVisible();
});
When('ユーザーが「カートに追加」ボタンをクリックする', async function (this: CustomWorld) {
await this.page.click('.add-to-cart-button');
});
Then('カートに{string}が{int}冊追加される', async function (this: CustomWorld, productName: string, quantity: number) {
await this.page.click('.cart-icon');
const cartItem = await this.page.locator(`.cart-item:has-text("${productName}")`);
await expect(cartItem).toBeVisible();
const quantityElement = await cartItem.locator('.quantity');
await expect(quantityElement).toHaveText(quantity.toString());
});
Then('カートアイコンに{string}と表示される', async function (this: CustomWorld, count: string) {
const badge = await this.page.locator('.cart-badge');
await expect(badge).toHaveText(count);
});
PlayWright:モダンなE2Eテストの実装
PlayWrightは、クロスブラウザ対応の強力なE2Eテストツールです。Cucumberと組み合わせることで、BDDシナリオを実際のブラウザ操作として実行できます。
World設定(テストコンテキスト)
// support/world.ts
import { World, IWorldOptions } from '@cucumber/cucumber';
import { Browser, BrowserContext, Page, chromium } from 'playwright';
export class CustomWorld extends World {
browser!: Browser;
context!: BrowserContext;
page!: Page;
constructor(options: IWorldOptions) {
super(options);
}
}
Hooks設定(前処理・後処理)
// support/hooks.ts
import { Before, After, BeforeAll, AfterAll } from '@cucumber/cucumber';
import { chromium, Browser } from 'playwright';
import { CustomWorld } from './world';
let browser: Browser;
BeforeAll(async function () {
browser = await chromium.launch({
headless: process.env.HEADLESS !== 'false',
slowMo: parseInt(process.env.SLOWMO || '0')
});
});
Before(async function (this: CustomWorld) {
this.context = await browser.newContext({
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
});
this.page = await this.context.newPage();
});
After(async function (this: CustomWorld, { result }) {
if (result?.status === 'FAILED') {
// 失敗時はスクリーンショットを保存
const screenshot = await this.page.screenshot({
path: `screenshots/failure-${Date.now()}.png`
});
await this.attach(screenshot, 'image/png');
}
await this.context.close();
});
AfterAll(async function () {
await browser.close();
});
実装フロー:初動で解像度をMAXにする
1. Example Mappingセッション
チーム全体で参加し、以下の要素を整理します:
- Rules(ルール): ビジネスルールや制約
- Examples(例): 具体的なシナリオ
- Questions(疑問): 不明確な点
2. シナリオの段階的詳細化
# 第1段階:ハッピーパスから始める
Scenario: 基本的な商品追加
When 商品をカートに追加する
Then カートに商品が入る
# 第2段階:具体的な値を追加
Scenario: 特定商品の追加
When 「JavaScript完全ガイド」を1冊カートに追加する
Then カートに「JavaScript完全ガイド」が1冊、3,800円で表示される
# 第3段階:エッジケースを追加
Scenario: 在庫切れ商品の追加試行
Given 「人気商品X」の在庫が0個
When 「人気商品X」をカートに追加しようとする
Then エラーメッセージ「在庫がありません」が表示される
And カートは空のまま
ベストプラクティス
1. シナリオは独立性を保つ
各シナリオは他のシナリオに依存せず、単独で実行可能にします。
2. 技術的詳細を避ける
# ❌ 悪い例
When ユーザーがid="submit-btn"のボタンをクリックする
Then HTTPステータス200が返される
# ✅ 良い例
When ユーザーが注文を確定する
Then 注文完了画面が表示される
3. タグを活用した実行制御
@smoke @critical
Scenario: ログイン機能
@slow @integration
Scenario: 外部API連携
# 実行時
npm run test -- --tags "@smoke and not @slow"
4. Page Object Patternの適用
// pages/ShoppingCartPage.ts
export class ShoppingCartPage {
constructor(private page: Page) {}
async addToCart(productName: string) {
await this.page.click(`[data-product="${productName}"] .add-to-cart`);
}
async getCartItemCount(): Promise<number> {
const text = await this.page.textContent('.cart-count');
return parseInt(text || '0');
}
async checkout() {
await this.page.click('.checkout-button');
await this.page.waitForURL('**/checkout');
}
}
CI/CD統合
GitHub Actions設定例
name: BDD E2E Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run BDD tests
run: npm run test:e2e
env:
HEADLESS: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: |
test-results/
screenshots/
- name: Generate Cucumber Report
if: always()
run: npm run report:generate
メトリクスとレポーティング
Cucumberレポートの生成
// cucumber.js
module.exports = {
default: {
require: ['features/**/*.steps.ts', 'support/**/*.ts'],
requireModule: ['ts-node/register'],
format: [
'progress',
'html:reports/cucumber-report.html',
'json:reports/cucumber-report.json',
'junit:reports/cucumber-report.xml'
],
parallel: 2
}
};
カバレッジメトリクス
トラブルシューティング
よくある問題と解決策
問題 | 原因 | 解決策 |
---|---|---|
ステップが見つからない | Step Definitionのパスが間違っている | cucumber.jsのrequire パスを確認 |
タイムアウトエラー | 要素の読み込みが遅い |
waitForSelector のタイムアウト値を調整 |
並列実行時の競合 | データの共有による競合 | 各テストで独立したテストデータを使用 |
フレーキーなテスト | 非同期処理の待機不足 | 適切な待機処理を追加 |
まとめ
BDD + Gherkin + Cucumber + PlayWrightの組み合わせは、プロジェクト初期から要求の解像度を最大化し、チーム全体で共通認識を持つための強力なアプローチです。
導入による効果
- コミュニケーションコストの削減: 仕様の認識齟齬が激減
- 早期の問題発見: 実装前に仕様の矛盾や不明瞭な点を発見
- 生きたドキュメント: 常に最新の仕様書として機能
- 品質の向上: 自動テストによる継続的な品質保証
初期投資は必要ですが、プロジェクトが進むにつれて、その価値は指数関数的に増大します。小さなプロジェクトから始めて、徐々に組織全体に展開していくことをお勧めします。