「Red-Green-Refactor 誰不知道?寫一個失敗的測試、讓它通過、再重構,這種基本功還需要花一篇文章講?」
如果你已經寫過幾年測試,這句話大概是你看到這篇標題時的第一反應。但我想先問一個問題:**Red 階段真正在確認的是什麼?**如果你的答案是「確認測試會失敗」,那只答對了一半——Red 階段確認的不是「測試失敗了」這個結果,而是「這個測試有能力偵測到錯誤」這件事本身。這兩者聽起來很像,實際上是完全不同的檢查。
昨天談到一個案例:一份測試的預期值是從待測程式碼自己算出來的,不管實作寫對還是寫錯,它永遠是綠燈。那份測試從來沒有真正紅過——如果作者曾經認真看過 Red 階段那顆紅燈長什麼樣子,這個問題在寫下第一行程式碼之前就會被抓到。**三個步驟的名字大家都背得出來,但每一步真正要確認的事,恰好是 AI 最容易悄悄跳過的地方。**這篇要做的事很單純:退回原點,把 Red、Green、Refactor 各自的目的講清楚,作為接下來 29 天檢查「AI 有沒有偷跳步」的判斷基準。
Kent Beck 在《Test-Driven Development: By Example》裡把 Red 階段描述為「寫一個小小的、跑不過的測試,甚至一開始可能連編譯都過不了」。這句話容易被簡化成「先寫測試」,但真正的重點在「跑不過」——而且不是隨便一種跑不過都算數。
Red 階段要確認兩件事:
換句話說,Red 階段是在幫測試本身做一次「測試的測試」。一份從來沒有紅過的測試,你永遠不知道它有沒有能力在實作寫錯的時候變紅——它可能寫了斷言、跑起來也是綠燈,但那顆綠燈可能只是因為斷言恆真、或是根本沒有真正呼叫到待測邏輯。看到紅燈,是你唯一能確認「這份測試有辨別力」的時刻,過了這個時間點再想確認就難了,因為之後每次跑測試看到的都是綠燈,你沒有機會反過來驗證它會不會變紅。
Beck 對 Green 階段的原話更直白:「盡快讓測試通過,過程中犯的任何罪都可以先不管」。這句話常被誤解成「隨便寫都行」,但真正的意圖是刻意壓抑「順便把其他情境也一起處理好」的衝動。
為什麼要這樣做?因為 Green 階段的唯一目標,是驗證「這個測試加上這段最小實作,兩者搭配起來是對的」。如果你在寫 Green 的當下就把三種邊界情境、兩種例外處理都一起寫進去,你就沒辦法確定測試綠燈是因為你剛剛寫的那個測試通過了,還是因為你多寫的那些程式碼裡剛好也包含了讓它通過的邏輯。最小手段讓測試通過,換來的是「這次的紅燈變綠燈,跟我剛剛寫的這段程式碼有直接對應關係」這件事的確定性。
至於「犯的罪」——寫死回傳值、複製貼上邏輯、暫時忽略某個分支——這些不是被鼓勵的壞習慣,而是被刻意允許的暫時債務,債務會在下一步被處理。
Refactor 階段是三步驟裡最常被跳過的一步,原因很現實:前兩步都有測試盯著(Red 要看到失敗、Green 要看到通過),但 Refactor 階段看起來「已經是綠燈了,好像做完了」。
但 Refactor 真正的定義是:在不改變外部行為的前提下,改善程式碼的內部結構。這句話裡「不改變外部行為」這個前提,靠的正是前面 Red 階段確認過的那份有辨別力的測試——因為你已經知道這份測試會在行為跑掉的時候變紅,你才能放心去動程式碼結構,而不用擔心自己在重構的過程中不小心改壞了行為。
這也是為什麼 Refactor 階段的存在,反過來證明了 Red 階段不能被跳過:如果測試本身沒有辨別力,Refactor 階段就沒有安全網可言,你做的每一次「重構」其實都是在裸奔改程式碼,只是你自己不知道。
下面用一個簡單案例,比較「照順序走過 Red」跟「直接寫出通過的程式碼跟測試」兩種做法的差別。情境是實作一個「檢查會員是否符合升級門檻」的函式,門檻是累積金額達到 1000 才算符合。
❌ 跳過 Red,測試和實作同時生成,從沒看過紅燈:
function checkUpgradeEligible(amount) {
return amount >= 1000;
}
test("checkUpgradeEligible", () => {
expect(checkUpgradeEligible(1000)).toBe(true);
});
→ 這份測試從第一次執行就是綠燈。
如果把 >= 誤寫成 >,這個測試依然是綠燈,
因為它只測了剛好等於門檻的那個值,
沒有人在寫測試的當下確認過「如果實作寫錯,這裡真的會變紅」。
✅ 先寫測試、看紅燈、再補最小實作:
test("累積金額等於門檻時應符合升級資格", () => {
expect(checkUpgradeEligible(1000)).toBe(true);
});
// 執行:ReferenceError,checkUpgradeEligible 尚未定義 → 紅燈,符合預期
// 補最小實作:
function checkUpgradeEligible(amount) {
return amount >= 1000;
}
// 執行:綠燈
// 再追加一個邊界測試,確認測試真的有辨別力:
test("累積金額差一元未達門檻時不應符合升級資格", () => {
expect(checkUpgradeEligible(999)).toBe(false);
});
→ 這一步才是關鍵:如果把 >= 誤寫成 >,
第一個測試依然綠燈,但第二個測試會告訴你「等於門檻」那個案例
其實對實作的正確性沒有鑑別力,必須靠邊界情境的測試補上。
這組對照要說的不是「要多寫一個測試案例」這麼表面的事,而是:只有在你認真經歷過「這個測試會不會紅」的檢查之後,才會發現原本那份測試根本測不出等號寫錯這種問題。跳過 Red 直接生出一組「看起來對」的測試加實作,恰好把這個檢查的機會整個跳過了。
Red 階段確認的不是測試失敗,而是測試有沒有能力偵測錯誤;少了這一步,Green 階段的通過跟 Refactor 階段的安全感,都建立在一個你從未驗證過的假設上。
這三個步驟的定義沒有變過,但 AI coding agent 介入之後,每一步都多了一種新的失守方式,這是接下來幾天要分別展開的內容,這裡先各留一句話當引子:
這三個挑戰的共同點,留到明天先講第一個切入點:AI 能不能加速「寫測試」這個動作,跟這份測試值不值得信任,是兩件事。
回想你最近一次寫測試的經驗:你有沒有真的停下來看過那顆測試變紅的那一刻?還是測試寫完、實作也順手寫完,第一次執行就已經是綠燈了?如果是後者,你怎麼確定這份測試在實作寫錯的時候真的會告訴你?
明天要談 AI 讓「寫測試」這個動作本身變快帶來的影響:AI 讓「寫測試」變快,但「寫對測試」的門檻沒有變低——動作變快跟品質變好,從來不是同一件事。
延伸閱讀:本系列另外三個並行系列——《用 AI Agent 重構一套無框架的 legacy PHP 系統》《讓 AI Agent 維護一個 Open Source Project》《AI 寫 Code 之後,我們還需要 Software Architecture 嗎?》——會從不同角度處理相關的經驗,有興趣可以一起追。
iThome鐵人賽