用 Uncle Bob 的 ATDD 紀律處理「高鐵早鳥票」

這套東西(Uncle Bob 的 ATDD 紀律,封裝成 Claude Code 的 atdd 外掛)要解的問題很單純:讓 AI 寫程式時,它會把程式碼亂塞、再補一個剛好會綠燈的單元測試;也會把實作細節(API、資料表)寫進規格。它的解法是兩條測試流(驗收 + 單元,兩條都得綠)、規格只用領域語言、再加變異測試。下面用高鐵早鳥票,從需求一路下指令走到綠燈。

〇、這份適合誰?動手前你需要什麼

先把話講白:這份適合誰 你要看得懂程式、會在專案裡跑測試。這套外掛(依官方定位)是給工程師用的——它把架構、行為契約、驗證門檻這些「工程判斷」交回你手上,所以完全不會寫程式的人,光照抄指令是不會成功的。但若你會寫程式、只是沒用過這個外掛、也沒做過 ATDD,那這份就能手把手帶你走。

動手前的前置準備(缺一不可)

  • 裝好 Claude Code,並能用它開啟你的專案資料夾。
  • 在一個 git 專案裡開 Claude Code——外掛是針對「你現有的程式庫」運作,不是憑空生出一個系統。
  • 先決定語言與測試框架(例如 Python + pytest、TypeScript + Jest),且本機能跑得起來。
  • 把「高鐵早鳥票」當成教學示範:它示範的是流程;凡是標「示意」的產出,會依你的專案而不同,不是保證一字不差。

一、需求敘述

先把需求講清楚——這是你要餵給系統的原始素材。

高鐵早鳥票(需求敘述) 旅客在乘車日前的「早鳥開放期間」內,於官網或 App 訂購『標準車廂、對號座、全票』時,系統應依當下早鳥配額,自動給予早鳥折扣:65 折配額有剩 → 給 65 折;否則 8 折有剩 → 給 8 折;否則 9 折有剩 → 給 9 折;三級都售罄 → 以原價售出(無早鳥)。商務車廂、自由座、優待票(敬老/愛心/孩童)、大學生優惠皆不適用早鳥,且早鳥不可與其他優惠合併。

整理成幾條規則

  • 適用對象:標準車廂、對號座、全票,且在早鳥開放期間內,且該級別配額還有剩。
  • 折扣優先序:65 折 → 8 折 → 9 折;都沒了就原價。
  • 不適用:商務車廂、自由座、優待票、大學生優惠;且不可與其他優惠合併。
先標成 PO 待確認的開放問題(別假設) 早鳥窗口的確切起訖日與邊界(含不含當天那一秒)、配額是各車次獨立還是共用、票價取整規則、「不可合併」的實際涵蓋範圍。這些確認前,後面 Examples 的預期結果都還是暫定。

二、把這套東西裝起來(一次性前置)

它是一個 Claude Code 外掛。在 Claude Code 裡先加 marketplace,再裝 atdd 外掛:

/plugin marketplace add swingerman/disciplined-agentic-engineering /plugin install atdd@disciplined-agentic-engineering

裝好後你會多出這些東西(後面會一一用到):

  • 指令:/atdd:atdd(啟動工作流)、/atdd:spec-check(檢查規格污染)、/atdd:mutate(變異測試)、/atdd:kill-mutants(補殺存活變異)。
  • 代理人:spec-guardian(揪出規格裡的實作細節)、pipeline-builder(產生你專案語言/框架的解析器與測試產生器)。

atdd 的工作流共七步:寫規格 → 產生測試管線 → 跑驗收測試(紅)→ 用 TDD 實作到兩流皆綠 → 檢查規格污染 → 變異測試 → 疊代下一個。下面從「挖 example」開始,照順序手把手走——步驟 0 屬上游的 engineer 外掛,步驟 1–7 才是 atdd 的工作流。

三、手把手:每一步下什麼 prompt、AI 會產出什麼

步驟 0 先用「四個面向」把 example 挖出來(engineer 外掛)

在寫規格之前,先把例子(驗收條件)從不同角度挖齊。這一步用上游的 engineer 外掛,正規順序是:先跟它討論點子並把需求落成 feature.md,再用四回合訪談挖出 AC。注意:需求是在 discuss 這一步交代的,discover-acs 會去讀 feature.md,不是把需求塞進它的指令參數。

你下的 prompt(依序):

# engineer 與 atdd 是分開的兩個外掛,會互通;需另外裝 /plugin install engineer@disciplined-agentic-engineering   # 1) 把點子與需求講給它,討論後 promote 成 feature, #    由 feature-init 寫進 feature.md: /engineer.discuss 高鐵早鳥票:旅客在早鳥開放期間訂標準車廂對號 全票,依配額給 65/8/9 折,售罄則原價;商務、自由座、優待票、 大學生優惠皆不適用,且不可合併。   # 2) 在這個 feature 上挖 AC(它讀 feature.md,不必再貼一次需求): /engineer.discover-acs

AI 會產出:

discover-acs 是「互動式」的:它讀著剛才寫好的 feature.md,用四回合一路反問你(快樂路徑 → 邊界 → 錯誤與安全 → 跨切面),你在對話裡把規則補齊,最後寫進 acs.md(純領域語言)。四個面向各自挖出一批 example:

面向挖什麼高鐵早鳥票的 example
快樂路徑最主流、最該先成立的窗口內、標準車廂對號全票、65 折有配額 → 訂到 65 折
邊界與例外臨界值、剛好那一刻、剩最後一個窗口第一天/最後一秒(半夜開賣的時區判定);65 折剛售罄跳 8 折;配額剩最後一張;跨日下單
錯誤與安全不該發生卻會發生、被惡意利用的付款逾時釋位;同一筆重複下單;高併發配額超賣;前端竄改折扣參數;非本人持證
跨切面與其他功能交互、一致性、非功能面與改票/退票/越乘的互動;不可合併優惠在 UI 與計價要一致;對帳與發票;多裝置分票
這一步的產物,就是步驟 1 的素材 acs.md 裡這些跨面向的 example,接著由 /engineer.atdd 橋接到 /atdd:atdd,被正式寫成 Gherkin 規格(也就是下一步)。這正是 SBE 講的「找出 key examples」——刻意從多個維度切,而不是只測一條路。

步驟 1 把 AC 形式化成 Gherkin 規格

這一步把行為寫成乾淨的 Given/When/Then(spec.md)。看你走哪一條路——兩條擇一,不要兩個都貼一次需求:

(A)接續步驟 0,完整 DAE 法:

/engineer.atdd      # 直接把 acs.md 形式化成 Gherkin,不必重貼需求

(B)跳過 engineer,精簡 atdd 法:

/atdd:atdd 高鐵早鳥票:旅客在早鳥開放期間訂標準車廂對號全票,依配額 給 65/8/9 折,售罄則原價;商務、自由座、優待票、大學生優惠不適用, 且不可合併。      # 由 atdd 一路帶你:規格 → 管線 → 紅綠 → TDD

AI 會產出:

不論哪一條,它都會帶你把需求寫成乾淨的 Given/When/Then 規格(寫進 spec.md),過程中 spec-guardian 會擋掉實作細節,並反問你那幾個待確認的點(窗口邊界、配額共用…)。產出大致像這樣,折扣分級用 Scenario Outline 展開:

Feature: 高鐵早鳥票訂購     Scenario Outline: 早鳥期間訂標準車廂對號全票,依配額給折扣     Given 現在在早鳥開放期間內     And 65折配額<q65>、8折配額<q8>、9折配額<q9>     When 我訂一張台北到左營的標準車廂對號全票     Then 我應該<結果>     Examples:       | q65 | q8 | q9 | 結果            |       | 有  | –  | –  | 訂到 65 折       |       | 無  | 有 | –  | 訂到 8 折        |       | 無  | 無 | 有 | 訂到 9 折        |       | 無  | 無 | 無 | 買到原價(無早鳥) |     Scenario: 商務車廂不適用早鳥     Given 現在在早鳥開放期間內     When 我訂一張商務車廂的車票     Then 我不應該拿到早鳥折扣

步驟 2 守門:檢查規格有沒有被實作污染

你下的 prompt:

/atdd:spec-check

AI 會產出:

spec-guardian 會逐條檢查,把混進規格的實作字眼挑出來,要你改回領域語言。它抓的就是這種對照:

被污染(它會標紅,要你改)改成領域語言(它要的)
When 對 /api/v2/booking 發 POST, body 帶 {fareType:”EB65″} Then bookings 表 discount=’EB65′When 我在早鳥期間訂一張標準車廂對號全票 Then 我應該訂到 65 折的早鳥票
重點 規格只描述「做什麼(WHAT)」。API、資料表、欄位這些「怎麼做(HOW)」,留到後面 step definition 那一層,規格層永遠保持乾淨。

步驟 3 產生這個專案專屬的測試管線

你下的 prompt(通常 /atdd:atdd 流程會接著做,也可明講):

請用 pytest 產生這個專案的驗收測試管線(parser → IR → 測試產生器)

AI 會產出:

pipeline-builder 會分析你的 codebase,產生你語言/框架專屬的解析器、JSON 中間表示法(IR)、與測試產生器。一條 scenario 解析後的 IR、以及產生的驗收測試骨架,大致像這樣(示意):

// JSON IR (示意) { “scenario”: “早鳥 65 折”,   “given”: [“在早鳥開放期間內”, “65折配額=有”],   “when”:  [“訂 台北→左營 標準車廂 對號 全票”],   “then”:  [“折扣 = 65折”] }   # 產生的驗收測試骨架 (示意, pytest) def test_早鳥_65折_有配額():     given_在早鳥開放期間內()     given_配額(q65=True)     r = when_訂票(起=”台北”, 訖=”左營”,                   車廂=”標準”, 座=”對號”, 票=”全票”)     then_折扣應為(r, “65折”) 
 唯一放實作的地方:step definition 「在早鳥期間內」「訂一張標準車廂對號全票」「折扣應為 65 折」這些領域語句怎麼對應到真正的呼叫與斷言,全部集中在 step definition。這層之外,看不到實作。

步驟 4 跑驗收測試 — 預期是紅燈

你做的事:

# 跑專案的 test runner (例如) pytest -k 早鳥

AI / 結果:

驗收測試會失敗(紅燈),因為功能還沒實作。這是對的——紅燈代表測試真的在驗東西,而不是空跑。Uncle Bob 的紀律就是:先看到紅,才有資格往下走。

步驟 5 用 TDD 實作,直到兩條測試流都綠

你下的 prompt:

用 TDD 實作高鐵早鳥票,直到驗收測試與單元測試都綠。 # 大型功能可改用團隊模式: /atdd:atdd-team   或直接說「build 高鐵早鳥票 with a team」

AI 會產出:

它會用 TDD 一邊長單元測試、一邊寫實作,把計價拆成可單獨測的小東西,直到驗收測試(WHAT)和單元測試(HOW)兩條流都綠。被它拆出來、各自有單元測試的單元大致是:

被測單元負責回答單元測試例子
isEarlyBirdEligible()夠不夠格吃早鳥?商務車廂 → false;標準對號全票且在窗口內 → true
pickTier()目前該給哪一級?65折有 → 65折;65折無、8折有 → 8折;全無 → 無早鳥
calcFare()折後票價多少?原價 1490、9 折 → 1340(取整規則待 PO 確認)
為什麼兩條都要綠 AI 沒辦法只塞一段程式、再配一個對自己有利的單元測試蒙混——因為驗收測試從外部行為同時把它夾住。兩條流一起綠,結構才算數。

步驟 6 變異測試 — 確認你的測試真的會咬人

你下的 prompt:

/atdd:mutate              # 跑變異測試 /atdd:mutate src/fare/    # 只測計價模組 /atdd:kill-mutants        # 針對存活的變異,補測試把它殺掉

AI 會產出:

它會故意把程式邏輯改壞(變異),看你的測試會不會抓到。抓到=殺死;沒抓到=存活(survivor),代表測試有破口。報告大致像這樣:

變異(把規則改壞)預期存活代表
早鳥窗口下限往外挪一天邊界 scenario 轉紅(殺死)邊界沒測到
拿掉「僅標準車廂」條件商務 scenario 轉紅(殺死)車廂限制沒測到
把 65 折與 8 折優先序對調配額 Examples 轉紅(殺死)折扣分級沒釘住

對存活的變異,/atdd:kill-mutants 會幫你補上能抓到它的測試。變異測試,是用來「測試你的測試」的。

步驟 7 疊代下一個情境

回到步驟 1,把下一條需求加進來(改車次、改座位、退票、越乘…),同一套流程再走一遍。規格越長越穩,因為每一條都被兩條測試流 + 變異釘住了。

四、收個尾:為什麼這樣走會穩

整條流程其實在做三件事:純領域規格擋住實作污染;兩條測試流從內外把結構夾住;變異測試證明測試真的會咬人。這正是 Specification by Example 的精神——用具體例子把需求釘死,讓 AI 和人都沒有模糊空間。

怎麼算「做完了」 判準很硬:驗收測試與單元測試「兩條都綠」,而且變異測試的存活者都被殺掉(/atdd:kill-mutants)。少一樣,都還沒完。

還有一點:範例裡用中文寫測試名稱與步驟,只是為了好讀;真實程式的識別字通常用英文,實際產出會依你專案的命名慣例與框架而定。

動手前再提醒一次 回到第一節那串 PO 開放問題:早鳥窗口邊界、配額共用與否、取整規則、不可合併範圍。這些一旦確認,Examples 的「預期結果」才算數——否則你只是把不確定,包裝成看起來很確定的綠燈。

發表迴響

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

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

Continue reading