在 Specification by Example(實例化需求)中,我們強調透過跨角色協作,找出能夠清楚展示功能在不同情境下應有行為的關鍵範例。這些範例不只是驗證功能是否能運作,更是幫助團隊理解背後業務規則的精髓。
但光有範例,還不足以讓我們完全放心系統的行為正確。這時,反例(Counter-example)就成了強化規則理解的重要武器。
反例用來展示「不符合條件、不應觸發該功能」的情境。它與傳統所謂的「負面測試(Negative Test)」不同,反例並不是次要的或可有可無的測試,它與正例一樣關鍵,因為反例能凸顯規則的邊界,避免模糊地帶。
假設我們要釐清一條業務規則:
規則: 早鳥票僅適用於出發前 28 天至 5 天內購票,且班次有早鳥票配額時,依票種享有 65 折、8 折或 9 折優惠。
第一個範例(正例)
• 情境: 出發日期是 5 月 30 日,今天是 5 月 10 日(距出發日 20 天),該班次尚有早鳥 65 折票。
• 結果: 顯示可選擇 65 折早鳥票。
光有這個範例,我們只能驗證「正常情況下可以買早鳥票」,卻無法確保系統在各種邊界與例外情況下表現正確。
搭配反例後的情境
變因 1:購票時間不符
• 反例 1: 出發日期是 5 月 30 日,今天是 4 月 25 日(距出發日 35 天)。
o 結果: 不顯示早鳥票(超過購票時間上限 28 天)。
• 反例 2: 出發日期是 5 月 30 日,今天是 5 月 28 日(距出發日 2 天)。
o 結果: 不顯示早鳥票(低於購票時間下限 5 天)。
變因 2:早鳥配額已滿
• 反例 3: 出發日期是 5 月 30 日,今天是 5 月 10 日(距出發日 20 天),但該班次早鳥票已售罄。
o 結果: 不顯示早鳥票選項。
變因 3:不同票種或限制
• 反例 4: 出發日期是 5 月 30 日,今天是 5 月 10 日,該班次僅剩 8 折早鳥票。
o 結果: 僅顯示 8 折早鳥票,不顯示 65 折。
在規劃範例與反例時,可以遵循這個流程:
(1) 從最簡單的正例開始
(2) 標註關鍵變數
(3) 建立表格
(4) 一次改變一個條件,產生反例
(5) 覆蓋所有輸入組合
在撰寫範例時,一個常見的陷阱,就是在範例中用數學公式或區間來描述情境分類。
很多時候,這是因為業務單位或分析師認為公式能「更完整」地描述規則。但實際上,公式往往只保留了規則的形式,卻失去了範例應該帶來的「真實情境感」。
高鐵訂票的真實案例
假設我們有一條業務規則:
規則: 早鳥票僅適用於出發前 28 天至 5 天內購票。
初學者可能會這樣在範例表格中呈現:
乍看之下很清楚,甚至感覺「很完整」。但問題是,這個表格只是重複了規則文字,沒有讓我們學到額外資訊,也沒有引發任何新的討論。
我們沒有機會去思考:
• 出發日與購票日是純日期還是精確到時間(timestamp)?
• 如果購票日是出發日的午夜 00:01,算哪一天?
• 在日光節約時間切換的那天會怎麼計算?
把公式換成實際日期,討論會立刻變得更具體:
這樣一來,團隊會馬上注意到:
• 我們需要定義日期計算的精度(天、時、分、秒)。
• 邊界條件是否包含當天?
• 票價計算是否受時區與日光節約時間影響?
這些細節若不在開發前討論清楚,後續上線可能就會發生爭議或 bug。
為什麼不要用公式當範例
• 公式不會引發邊界討論
用 出發日 - 28 ≤ 購票日 ≤ 出發日 - 5,看似完整,但所有邊界細節都被模糊掉了。
• 容易隱藏假設
沒有人會因為公式而自然想到「午夜算不算當天?」或「日光節約時間怎麼辦?」。
• 可能誤導團隊
看起來像是已經覆蓋全部情境,但其實只是把需求文字換個形式重述一遍。
如何讓範例更有價值
(1) 公式只當起點,不是最終範例
範例的力量在於讓抽象的規則落地成可以討論、可以驗證的情境。如果範例只是用數學公式重述需求,那它就只是另一種「需求文件」,而不是能揭露邊界與假設的討論工具。
在撰寫範例時,如果一個範例涵蓋了太多步驟與行動,就很容易失去焦點。
這類測試的症狀通常是:
• Given–When–Then 中出現多個 When 子句
• 單一 When 句子用「而且(And)」或「以及」串了好幾個行動
• 驗證涵蓋了不同主題,但被寫在同一個情境裡
問題是,這樣的測試往往緊密耦合到特定技術流程,一旦實作細節改變,即使業務規則不變,測試也可能整串失效。
假設我們有一個自動化驗收測試,想確認高鐵從訂票到上車的流程:
When 使用者選擇台北到高雄的班次
And 選擇早鳥票
And 完成付款
And 收到電子票號
And 進站驗票
And 坐上指定班次
看似一步到位,但問題是:
• 牽涉到 選班次、選票種、付款、進站驗票、搭車 這些完全不同的動作
• 任何一個步驟失敗,都可能讓後面全部驗證失效
• 只要高鐵改了其中任一步(例如改成付款後自動進站),整個測試就得大改
我們可以把上面的流程拆成三個獨立測試:
測試 1:選票成功
Given 使用者已登入並查詢到班次
When 選擇早鳥票
Then 系統顯示票價與座位資訊
測試 2:付款成功
Given 已選好票種與座位
When 使用者完成信用卡付款
Then 系統顯示付款成功
And 發送電子票號
測試 3:進站驗票成功
Given 使用者持有有效電子票號
When 進站驗票
Then 驗票閘門開啟
為什麼要這樣拆
• 好維護:如果付款流程改版,只要改「付款」的測試,不會影響選票與進站的測試
• 邊界更好測:專注測付款時,可以額外加上「付款失敗」、「卡片過期」等情境
• 回饋更快:開發付款功能時,只跑付款的測試即可,不必等選票與進站也測完
在高鐵訂票系統這樣的真實業務中,一個測試專注於一件事,可以讓需求釐清更有焦點、測試更容易維護、也能更快回饋問題。
與其寫一個「看似完整」的大測試,不如拆成多個小而聚焦的測試,才能真正發揮自動化驗收的價值。
在高鐵訂票系統中,一個使用者故事很可能同時包含業務面和技術面的需求。
例如:
• 業務需求:顧客成功購買早鳥票後,系統必須在 Email 發送電子票號。
• 技術需求:Email 必須依照特定的 XML 格式傳送,並透過指定的通訊協定發送到郵件伺服器。
兩者都很重要,也都需要測試,但如果把它們放在同一個測試裡,會發生什麼事呢?
假設你的測試流程是這樣:
When 顧客購買早鳥票
Then 系統以 XML 格式發送電子票號到 Email
And 驗證 XML 格式符合規範
And 驗證 Email 成功送達
這樣的測試同時檢查了:
• 業務流程(早鳥票購買成功 → 發送電子票號)
• 技術細節(XML 格式、郵件送達)
結果是:
• 驗證 XML 邊界條件(例如欄位順序、大小限制)變得很慢,因為必須經過整個訂票流程才能測
• 小小的技術改動(例如 XML 欄位多了一層巢狀結構)就會讓整個業務流程測試失敗,即使業務規則沒有變
• 業務專家在檢查測試結果時,會被 XML 細節淹沒,難以提供有效意見
如果我們將測試分成兩組:
(1) 業務測試(Business Check)
用一般人能讀懂的方式描述情境與結果
Given 顧客購買早鳥票成功
Then 系統發送電子票號到 Email
這讓業務專家可以檢查「在什麼情況下該發票號、什麼情況下不該發」
(2) 技術測試(Technical Check)
專注檢查 XML 格式、通訊協定、欄位長度、非同步處理等
Given 系統要發送電子票號
When 產生 XML 訊息
Then XML 結構符合規範
And 所有必填欄位存在
這讓工程師可以快速檢查技術細節,而不必經過完整業務流程
如何判斷該放哪一邊?你可問自己:如果測試失敗,需要誰來修?
• 技術團隊 → 技術測試
• 業務團隊 → 業務測試
我們可以用高鐵訂票的案例來看看:
把技術檢查和業務檢查分開,就像把「菜單設計」和「廚房烹飪流程」分成兩份文件,各自優化、各自維護,誰出錯就找誰改,不會彼此拖累。