情境背景
註冊流程要加一條新規則:使用者信箱必須是公司網域(@example.com)才能完成註冊。你想用測試驅動開發(Test-Driven Development)的方式加這個行為,避免之後改規則時沒有測試保護。
/tdd 的核心是 red → green 迴圈,refactor 階段已經移交給 /code-review。重點不是「寫很多測試」,而是「一次一個垂直切片,每片都有真實失敗的 red 與最小通過的 green」。
第 1 步:與 agent 確認 seam
目標:在寫任何測試前,先決定測試要打哪一層
AI 對話:
-
You:
/tdd 我想加「信箱必須是 @example.com 網域才能註冊」這個行為。 -
AI:
先確認 seam。這個驗證規則的測試應該打哪一層?
我的提案:純邏輯函式
isAllowedEmail(email),輸入字串、輸出布林,不碰 DB 也不碰 UI。seam 是這個函式的輸入輸出。同意嗎? -
You:
同意,先以純函式為 seam。註冊流程之後再接。
確認 seam 是 /tdd 的鐵律。沒有這一步,agent 不會往下走。
第 2 步:Red,第一個切片
目標:寫一個會失敗的測試,描述接下來要加的行為
AI 對話:
-
AI:
Red:
test('公司網域信箱通過', () => { expect(isAllowedEmail('[email protected]')).toBe(true); });預期值
true來自獨立判斷(你告訴我的規則),不是重跑程式碼。跑一下確認它因為對的原因失敗。 -
You:
失敗了。錯誤訊息是
isAllowedEmail is not defined。 -
AI:
這是正確的 red。函式還不存在,符合預期。
第 3 步:Green,最小實作
目標:寫剛好讓測試通過的程式碼,不多寫
AI 對話:
-
AI:
Green:
export function isAllowedEmail(email: string): boolean { return email.endsWith('@example.com'); }跑測試。
-
You:
通過了。
注意 Green 階段不偷寫下一個切片。如果你想順手加「不允許外部網域」的判斷,那是下一個 red 的事。
第 4 步:Red,第二個切片
目標:為反向情境寫測試
AI 對話:
-
AI:
Red:
test('外部網域信箱不通過', () => { expect(isAllowedEmail('[email protected]')).toBe(false); }); -
You:
失敗。實際回傳
true,預期false。 -
AI:
等等,這代表現有實作有 bug。
endsWith不會區分大小寫,而且[email protected]這類前置攻擊也會通過。我們先修最小切片,再處理邊界。 -
AI:
Green:
export function isAllowedEmail(email: string): boolean { return /^[^@]+@example\.com$/.test(email); }跑測試。
-
You:
兩個切片都通過。
第 5 步:Red,第三個切片(大小寫)
目標:處理大小寫邊界
AI 對話:
-
AI:
Red:
test('大寫網域也應通過', () => { expect(isAllowedEmail('[email protected]')).toBe(true); }); -
You:
失敗。實際
false,預期true。 -
AI:
Green:
export function isAllowedEmail(email: string): boolean { return /^[^@]+@example\.com$/i.test(email); }
反模式提醒
同義反覆測試(tautological test)
預期值用同一個函式再算一遍,這種測試不可能失敗,給零信心。預期值必須來自獨立來源,例如規則文件、手算、或已知正確的參考實作。
橫向鋪層而非垂直切片
把整個資料層寫完、再寫 service、再寫 UI,中間沒有任何可運作的切片。/tdd 要求每一圈都是端到端的細長功能,這樣才能在每個切片後看到測試回饋。
漂浮的 refactor
在 red → green 之間偷改結構。refactor 屬於 /code-review 階段,不在這裡。想重構就用 /code-review 收尾。
工具使用摘要
| Skill | 用途 | 在本例的作用 |
|---|---|---|
| tdd | red → green 迴圈 | 三個垂直切片,每片都有獨立預期值的 red 與最小 green |
| code-review | refactor 收尾 | tdd 結束後接手,檢查命名與重複 |
結果
- 三個垂直切片都完成:合法網域、外部網域、大小寫邊界
- 全程沒有漂浮的 refactor,refactor 留給後續
/code-review - seam 是純函式層,未來要接註冊流程時不會被測試綁死
關鍵學習點
- 寫測試前先與 agent 確認 seam,是
/tdd的鐵律。沒有 seam 共識就不開始。 - 預期值必須來自獨立來源,不能用同一份邏輯再算一遍當 oracle。
- Green 只為了讓紅燈轉綠,不偷寫下一個切片,也不趁機 refactor。
- 三個反模式要記得:同義反覆測試、橫向鋪層、漂浮 refactor。