為什麼你的 Example 越寫越複雜?用「高鐵多人分票」一次講清楚 SbE 的正確寫法與常見陷阱

在推動 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 同時符合以下兩點,很高機率是可測性問題

  1. Given 裡充滿 UI 操作與跨系統流程
  2. Given 的存在目的只是在「把狀態弄出來」

這表示:
你真正缺的是「能快速建立狀態的手段」,例如:

  • Test Data Builder / Factory(直接生成已付款訂單)
  • API / Service 層測試入口(建立訂單、設狀態、設票數)
  • Seed Data / Fixture(預先準備測試資料)
  • Stub Notification(金流、簡訊不必真打)

當這些不存在,Given 就會爆炸。




八、總結

如果你只記得一句話,請記這句:

真實世界需求確實複雜,但好的 Example 必須刻意簡單;若 Example 越寫越像操作腳本,問題往往不在需求,而在系統可測性不足。

發表迴響

探索更多來自 轉念學 - 敏捷三叔公的學習之旅 的內容

立即訂閱即可持續閱讀,還能取得所有封存文章。

Continue reading