在推動 Specification by Example(實例化需求)或 BDD 的過程中,我最常聽到兩種完全相反的聲音:
有人說:「Example 不要太複雜,越簡單越好。」
也有人說:「真實世界需求本來就很複雜,Example 當然會很複雜。」
兩邊聽起來都很有道理,卻也都讓人困惑。
尤其當你開始處理像「訂票、金融、保險」這類高規則密度系統時,Example 很容易從三行 Given-When-Then,膨脹成一頁操作流程。
那到底是:
- 真實世界本來就複雜?
- 還是我們寫 Example 的方式出錯?
- 還是其實是系統可測性在拖累?
這篇我用一個很貼近台灣情境的功能來拆解:台灣高鐵訂票系統:多人分票。
一、先釐清一件事:系統複雜 ≠ 單一 Example 要複雜
多人分票這個功能,真實世界一定複雜:
- 訂單狀態(已付款 / 未付款 / 取消)
- 票券狀態(未分配 / 已分配 / 已領取)
- 收票者識別(手機 / Email / 會員)
- 一人多票 / 一票多人限制
- 通知發送(簡訊 / Email / 推播)
- 分配後是否可取消
這些全部都是真的。
但 SbE 的精神不是把所有複雜塞進一個 Example,而是:把複雜拆開,放到正確層次。
二、Discovery 階段:Example 應該寫到什麼程度?
Discovery(需求釐清)階段的 Example,有三個目的:
1️⃣ 對齊業務理解
2️⃣ 逼出規則
3️⃣ 找出模糊地帶
它不是用來驗證系統,也不是測試腳本。
所以 Example 應該:
- 短
- 聚焦一條規則
- 用狀態描述背景
- 不描述操作流程
三、完整案例:多人分票的 3 條核心規則
我們先切出一個可交付範圍:
本次只處理:
- 已付款訂單
- 1–10 張票
- 可分配給收票者(手機為主)
- 分配後可發送領票通知
規則 1:只有已付款訂單可分票
Example 1(正向)
Given 訂單狀態 = 已付款,含 3 張票
When 訂購人開啟多人分票
Then 系統允許分配每張票
Example 2(反向)
Given 訂單狀態 = 未付款,含 3 張票
When 訂購人開啟多人分票
Then 系統提示需先付款,且不可分配
規則 2:一張票只能分配給一個人
Example 3
Given 票1、票2 尚未分配
When 將票1 分配給 A
Then 票1 顯示已分配給 A,票2 仍可分配
Example 4
Given 票1 已分配給 A
When 嘗試將票1 再分配給 B
Then 系統拒絕並提示此票已分配
規則 3:同一收票者可持有多張票
Example 5
Given 訂單含 3 張票,皆未分配
When 票1、票2 分配給 A
Then A 會收到 2 張票的領票資訊
Example 6(識別釐清)
Given A 手機 = 09xx,Email = a@x.com
When 票1 用手機分配給 A,票2 用 Email 分配
Then 系統需符合識別規則(合併 / 不合併需明確定義)
到這裡,你會發現:
- 我們已經釐清 3 條關鍵規則
- 用 6 個 Example 就能講清楚
- 每個 Example 都很短
這就是 Discovery 應該停下來的地方。
四、Deliver 階段:為何 Example 會變多、變細?
交付階段要處理的不只是理解,還包括:
- 驗收
- 自動化
- 邊界驗證
- 錯誤處理
這時候「複雜度會上升」是合理的。
但專家不會把複雜塞進 Scenario,而是改用結構化方式。
五、Deliver 補強 1:Decision Table(輸入驗證)
例如收票者手機輸入驗證:
| 手機格式正確 | 是否已存在 | 結果 |
|---|---|---|
| 否 | – | 顯示格式錯誤 |
| 是 | 否 | 建立新收票者 |
| 是 | 是 | 使用既有收票者 |
這張表就取代了至少 6 個 scenario。
六、Deliver 補強 2:狀態轉移清單(State Transition)
多人分票一定會涉及狀態流轉:
票券狀態
- 未分配
- 已分配
- 已領取
- 已使用
狀態轉移規則
| 起始狀態 | 動作 | 結果狀態 |
|---|---|---|
| 未分配 | 分配給收票者 | 已分配 |
| 已分配 | 收票者領取 | 已領取 |
| 已領取 | 進站使用 | 已使用 |
| 已分配 | 取消分配 | 未分配(若規則允許) |
這些是 Deliver 必須補齊的,但不需要寫成長長的 Scenario。
七、錯誤寫法:為什麼很多 Example 會失控?
來看一個真實專案常見的錯誤 Example:
Given 使用者登入
And 查詢班次
And 訂 3 張票
And 信用卡付款
And 返回訂單頁
And 新增收票者 A
And 新增收票者 B
And 分配票
And 重送通知
這種寫法問題不在需求,而在兩件事:
錯誤 1:把測試流程當需求 Example
Example 應該描述規則,而不是操作步驟。
錯誤 2:系統可測性不足
很多系統無法:
- 直接建立已付款訂單
- 直接設定票券狀態
- 直接建立收票者
只能走完整 UI 流程。於是 Given 被迫變長。
當系統可測性不高,你很難直接把背景狀態設好,例如:
- 你無法直接建立「已付款訂單」
- 你無法直接建立「包含 3 張票、其中 1 張已分配」的狀態
- 你必須走完整 UI 流程才做得到
這會導致:Given 被迫寫成一串操作流程,SbE 退化成 Scripted Testing,Example 失去溝通與釐清規則的功能
換句話說:
Example 變複雜,不是不夠聰明,是系統讓你只能這樣寫。
你可以用這個簡單的現場診斷:如果 Given 同時符合以下兩點,很高機率是可測性問題
- Given 裡充滿 UI 操作與跨系統流程
- Given 的存在目的只是在「把狀態弄出來」
這表示:
你真正缺的是「能快速建立狀態的手段」,例如:
- Test Data Builder / Factory(直接生成已付款訂單)
- API / Service 層測試入口(建立訂單、設狀態、設票數)
- Seed Data / Fixture(預先準備測試資料)
- Stub Notification(金流、簡訊不必真打)
當這些不存在,Given 就會爆炸。
八、總結
如果你只記得一句話,請記這句:
真實世界需求確實複雜,但好的 Example 必須刻意簡單;若 Example 越寫越像操作腳本,問題往往不在需求,而在系統可測性不足。
發表迴響