行為驅動開發(BDD, Behaviour-Driven Development)與實例化規格(Specification by Example)關注的是測試金字塔最上層的「驗收層」:不是「程式碼寫對了嗎」,而是「我們有沒有做對的東西」。它們把需求、測試、文件三者合而為一,讓業務、開發、測試講同一種語言。
本章聚焦驗收層的協作與規格;單元層的 given/when/then 與 Java 實作見 Java 單元測試深度章節。兩者分層互補——詳見文末「與單元層的分層關係」。
BDD 的核心價值:不只是自動化#
最常見的誤解,是把 BDD 當成「一種自動化測試工具(Cucumber)」。自動化只是它的副產品,真正的價值在前面:
BDD 的核心,是用一套業務可讀、結構固定的語言,逼著團隊在動工前把「完成的定義」談清楚。一份 Given-When-Then 同時是:需求(業務看得懂)、測試(機器可執行)、文件(永遠跟程式碼同步)。三合一,消除了三份文件互相對不上的老問題。
flowchart LR
A[需求 / 期望行為] --> B[用具體實例闡明]
B --> C[Given-When-Then 規格]
C --> D[自動化驗收]
D --> E[活文件<br/>Living Documentation]
E -.持續回饋.-> A| BDD 帶來的轉變 | 說明 |
|---|---|
| 需求三合一 | 同一份規格既是需求、又是測試、又是文件 |
| 對話優先 | 價值多半在「寫規格前那場對話」,而非規格檔本身 |
| 由外而內 | 從使用者可見的行為往內驅動開發,避免做出沒人要的功能 |
| 文件不腐爛 | 規格由自動化執行把關,過時了就會紅燈,不會悄悄失真 |
Gherkin 慣例:Given-When-Then#
Gherkin 是描述行為的結構化語言,核心關鍵字三個:
- Given:前置情境(系統已處於什麼狀態)
- When:觸發事件(使用者/系統做了一個動作)
- Then:預期結果(應觀察到什麼)
Feature: 購物車金額計算
作為顧客
我希望結帳金額正確反映優惠
以便信任這個系統
Scenario: 一般顧客滿千未享免運
Given 我是一般會員
And 購物車金額為 800 元
When 我前往結帳
Then 運費應為 60 元
Scenario Outline: 依會員等級計算運費
Given 我是 <會員等級> 會員
And 購物車金額為 <金額> 元
When 我前往結帳
Then 運費應為 <運費> 元
Examples:
| 會員等級 | 金額 | 運費 |
| 一般 | 800 | 60 |
| 一般 | 1200 | 0 |
| VIP | 100 | 0 || 結構 | 用途 |
|---|---|
| Feature | 一個功能模組,含「角色—期望—價值」的敘事 |
| Scenario | 單一具體情境 |
| Scenario Outline | 同一情境結構、多組資料,用 Examples 表展開(資料驅動) |
| Background | 多個 Scenario 共用的前置 Given(避免重複) |
Gherkin 反模式:命令式 vs 宣告式#
寫 Gherkin 最大的陷阱,是把它當成「點按操作腳本」來寫(命令式),而不是「描述業務意圖」(宣告式)。命令式 scenario 又長又脆——UI 一改全爆,而且讀者看完仍不知道這條規則在講什麼。
# ✗ 命令式(imperative):描述「怎麼操作」,混入 UI 細節
Scenario: 結帳免運
Given 我打開瀏覽器前往首頁
And 我點擊「登入」按鈕
And 我在帳號欄輸入 "vip01"
And 我在密碼欄輸入 "pw"
And 我點擊「送出」
And 我搜尋商品 "滑鼠"
And 我點擊「加入購物車」
When 我點擊「前往結帳」
Then 我看到運費欄顯示 "0"# ✓ 宣告式(declarative):描述「業務意圖」,與 UI 解耦
Scenario: VIP 顧客享免運
Given 我是 VIP 會員
And 購物車內有商品
When 我前往結帳
Then 運費應為 0 元判斷標準:把網站換成 App、把按鈕換成語音指令,這條 scenario 還成立嗎? 宣告式的會;命令式的會整條報廢。操作細節屬於步驟定義(step definition)那層的實作,不該出現在規格裡。
實例化規格的七個實踐#
實例化規格是一套讓「規格」從需求一路活到生產的協作流程,可歸納為七個環環相扣的實踐:
| # | 實踐 | 重點 |
|---|---|---|
| 1 | 從目標推導範圍 | 先問業務目標,由目標反推該做哪些功能,而非照單全收需求 |
| 2 | 協作制定規格 | 業務、開發、測試一起定義規格,而非單方丟需求文件 |
| 3 | 用實例闡明 | 用具體例子(含邊界、例外)說清楚規則,消除模糊地帶 |
| 4 | 精煉規格 | 去掉操作雜訊與重複,留下宣告式、聚焦業務意圖的關鍵實例 |
| 5 | 自動化驗證 | 把精煉後的規格接上自動化,使其可重複執行 |
| 6 | 頻繁驗證 | 每次變更都跑規格,確保系統與規格始終一致 |
| 7 | 演進出活文件 | 規格隨系統持續更新,沉澱為可信賴的活文件 |
flowchart TD
G[1. 從目標推導範圍] --> C[2. 協作制定規格]
C --> I[3. 用實例闡明]
I --> R[4. 精煉規格]
R --> A[5. 自動化驗證]
A --> F[6. 頻繁驗證]
F --> L[7. 演進出活文件]
L -.下一輪迭代.-> G活文件(Living Documentation)#
活文件是上述流程的最終產物:一份會被自動化持續驗證、因而永遠與系統行為一致的說明文件。
| 傳統文件 | 活文件 |
|---|---|
| 寫完就開始腐爛、與程式碼脫節 | 過時就紅燈,被迫保持同步 |
| 沒人信任、沒人讀 | 是唯一可信的行為來源(single source of truth) |
| 維護是額外負擔 | 維護即開發流程本身,無額外成本 |
活文件讓「文件」從成本變成資產:新成員讀規格就能理解系統行為,業務看規格就能確認需求,而你完全不必擔心它在說謊——因為說謊的那一刻它就會讓建置失敗。
貫穿例子:VIP 訂單免運費#
以下示範一條業務規則如何走完「規格 ➡️ Gherkin ➡️ 驗收」。
1. 規格(協作後用實例闡明的規則)
VIP 會員一律免運費;一般會員滿 1000 元免運,未滿收 60 元。
2. Gherkin(精煉成宣告式、含邊界實例)
Feature: 訂單運費規則
Scenario Outline: 依會員等級與金額計算運費
Given 我是 <等級> 會員
And 購物車金額為 <金額> 元
When 我前往結帳
Then 運費應為 <運費> 元
Examples:
| 等級 | 金額 | 運費 |
| VIP | 100 | 0 |
| 一般 | 999 | 60 |
| 一般 | 1000 | 0 |3. 驗收(步驟定義把規格綁到系統)
public class ShippingFeeSteps {
private Cart cart;
private int actualFee;
@Given("我是 {word} 會員")
public void setMembership(String level) {
cart = new Cart(MemberLevel.from(level));
}
@Given("購物車金額為 {int} 元")
public void setAmount(int amount) {
cart.setSubtotal(amount);
}
@When("我前往結帳")
public void checkout() {
actualFee = new ShippingPolicy().calcFee(cart);
}
@Then("運費應為 {int} 元")
public void verifyFee(int expected) {
assertEquals(expected, actualFee);
}
}注意
999與1000兩個 Examples——它們把「滿 1000」這條規則的邊界值直接寫進了活文件。任何人讀規格都能立刻看出臨界點在哪,這正是「用實例闡明」消除模糊的威力。
與單元層的分層關係#
驗收層的 Gherkin 與單元層的 given/when/then 形式相近,但目的與粒度不同,兩者協同覆蓋不同風險。
| 維度 | 驗收層(本章 Gherkin) | 單元層(07 Java 章) |
|---|---|---|
| 回答的問題 | 有沒有做對的東西(外部行為) | 程式碼有沒有寫對(內部邏輯) |
| 語言 | 業務可讀的自然語言 | 開發者導向的測試程式碼 |
| 粒度 | 端到端的業務情境 | 單一函式/類別的行為 |
| 速度/數量 | 較慢、數量少(金字塔頂) | 極快、數量多(金字塔底) |
| 讀者 | 業務 + 開發 + 測試 | 開發者 |
flowchart TD
A[驗收層: Gherkin 業務情境<br/>確認做對的東西] --> B[單元層: given/when/then<br/>確認程式碼寫對]
A -.少量, 慢.-> A
B -.大量, 快.-> B別把所有規則都推到驗收層測——那會得到一堆又慢又脆的端到端測試。驗收層只覆蓋關鍵業務情境(活文件價值最高處),細節邏輯與邊界組合下沉到單元層,符合測試金字塔。