「要先寫測試?程式碼都還沒有,是要測試什麼?」
第一次接觸 TDD(Test-Driven Development,測試導向開發)時,任何人都會這麼想。順序看起來反了,還會覺得無緣無故把工作量增加了一倍。
不過,實際投入幾個月後,想法會稍微改變。
TDD 是一種反覆執行極短迴圈的開發方式:「失敗的測試(Red)→ 讓測試通過的最少程式碼(Green)→ 整理(Refactor)」。重點是以幾秒到幾分鐘的速度快速跑完這個迴圈。
今天就來一步步看看這個 Red–Green–Refactor 迴圈實際上如何運作。
TDD Red–Green–Refactor:3 個步驟重點整理 ✍️
先讓你一眼看懂完整迴圈。
- Red:先為尚不存在的功能撰寫測試。當然會失敗。
- Green:只撰寫能讓測試通過的最少程式碼。不必漂亮。
- Refactor:維持測試通過的狀態,同時整理程式碼。
把這 3 個步驟視為一個整體,不斷重複即可。
重要的是不要把每個步驟訂得太大。不要一次處理「完整的登入功能」,而是拆成「電子郵件為空時回傳錯誤」這種小單位。
如果一個迴圈要花 30 分鐘,那很可能只是搭配測試的開發,而不是 TDD。
Red 階段,為什麼要故意撰寫會失敗的程式碼?
這是初學 TDD 時最難理解的部分。明知道會失敗,為什麼還要執行?
原因很簡單。為了確認這個測試「確實會失敗」。
如果測試一寫好就通過,實際上它可能只是什麼都沒驗證的空殼。先看到失敗,等於是在測試測試本身。
舉個簡單的例子,假設要建立一個將兩個數字相加的函式。會先出現測試,再出現程式碼。
// add 函式尚不存在。因此這個測試會失敗(Red)
struct AddTests {
@Test func 加法_1相加2是3() {
#expect(add(1, 2) == 3)
}
}
在這個狀態下執行,會出現像「cannot find ‘add’ in scope」這樣的錯誤。反而是看到這個紅燈,會讓我更安心,因為目標變清楚了。
從現在開始,我要做的只有一件事:把紅燈變成綠燈。
Green 與 Refactor,真正的實力就在這裡分出高下
Green 階段的原則是「簡單到令人不好意思」。
讓前面測試通過的最快程式碼就是這樣。
// 讓測試通過所需的最少程式碼(Green)
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
看起來很理所當然吧?但越是初學者,越容易在這裡擔心未來,提前加入例外處理、記錄與設定值。
TDD 會刻意讓你忍住這個衝動。規則就是:不要撰寫目前測試沒有要求的程式碼。
等綠燈亮起後,才進入 Refactor。這時會調整變數名稱、移除重複內容。
重點是,Refactor 期間絕對不新增功能。只在維持測試通過的狀態下改善結構。
如果整理時亮起紅燈呢?把剛才修改的內容還原就好。測試會保證它不久前還能正常運作。
多虧這個安全網,我重構時敢大膽得多。
使用 TDD,開發不會變慢嗎? 🤔
這是最常被問到的問題。實際使用後,我的答案是:「要看情況。」
我把自己感受到的差異整理成表格。
| 分類 | 套用 TDD | 之後再撰寫測試 |
|---|---|---|
| 初期速度 | 稍慢 | 快 |
| 發現錯誤的時機 | 撰寫後立即 | 過了很久之後 |
| 重構負擔 | 低 | 高 |
| 複雜邏輯 | 有利 | 不利 |
老實說,快速做出一個單純畫面時,TDD 有時會讓人覺得礙手礙腳。
相反地,在付款、結算、折扣計算等條件交錯的邏輯中,TDD 確實發揮了價值。用測試逐一鎖定各種情況後,即使之後大幅修改程式碼,也不再害怕。
最後,TDD 不是萬靈丹,而更像是「駕馭複雜度的工具」。
總結
如果覺得 TDD 很難,別一開始就設定得太宏大,先從一個函式跑一次 Red–Green–Refactor 吧。
當小小的成功迴圈成為習慣,就會稍微理解為什麼人們無法放棄這種方法。今天就從一個測試開始點亮紅燈吧。

