這套東西(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 的「預期結果」才算數——否則你只是把不確定,包裝成看起來很確定的綠燈。 |
發表迴響