TDD 不是「先寫測試」這麼簡單,而是一種由測試驅動設計的節奏:先寫一個會失敗的測試(紅),用最簡陋的方式讓它通過(綠),再回頭重構消除重複(重構)。每一圈都小到幾乎可笑——而正是這種小步伐,讓你隨時都站在可運作的地面上。
🧠 Why It Matters
- 測試先行 = 設計先行:先寫測試,等於先站在使用者的角度決定 API 該長什麼樣。程式碼還沒寫,可測性就已經被設計進去了(Clean Code FIRST 的 T:Timely)。
- Kent Beck 的五步循環:① 快速新增一個測試 → ② 執行、看它失敗 → ③ 做最小修改讓它通過 → ④ 再跑一次、全部綠 → ⑤ 重構消除剛才為了通過而製造的重複。
- 綠燈階段允許「作弊」:為了通過測試,修改可以簡陋到直接回傳常數(fake it)。重點是先拿到綠燈這個安全網,再在重構階段把它長成真正的實作。
- 步伐會越走越小:資深 TDD 開發者的趨勢很明確——步伐越來越小。手動重構容易出錯,錯越晚發現越不敢重構;有自動化重構工具撐腰,你才敢大膽。
TIP
什麼不用測?Phlip 的答案:「寫測試直到恐懼轉化為無聊。」該測的是你自己寫的條件判斷、迴圈、運算、多型;別去測你信任的第三方程式碼——除非你想用一個會失敗的測試來標記它的 bug 被修好了。
⚖️ 一圈紅綠重構
① 紅 — 先寫一個會失敗的測試(API 還不存在)
// 連 Money 都還沒寫,測試先定義「我希望怎麼用它」
test("5 USD times 2 is 10 USD", () => {
const five = money(5, "USD");
expect(five.times(2)).toEqual(money(10, "USD"));
});
// ▶ 執行 → 失敗(紅):money is not defined。這是預期中的失敗。② 綠 — 用最小、甚至簡陋的實作讓它通過
// 不追求漂亮,只求綠燈。允許先寫死、之後再長出來。
function money(amount, currency) {
return {
amount,
currency,
times(n) {
return money(amount * n, currency);
},
};
}
// ▶ 執行 → 通過(綠)。現在有安全網了。③ 重構 — 綠燈保護下消除重複、改善設計
// 有了通過的測試,才敢動結構。例如把「相等比較」抽出來共用,
// 或把建構邏輯收斂——每改一步就重跑測試,始終保持綠。
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
times(n) {
return new Money(this.amount * n, this.currency);
}
equals(other) {
return this.amount === other.amount && this.currency === other.currency;
}
}
const money = (amount, currency) => new Money(amount, currency);
// ▶ 重跑 → 仍綠。重構不改變行為,只改善結構。重構這一步靠的正是 Extract Function 這類手法,而測試就是讓你敢動手的那張網。
IMPORTANT
三個階段的紀律:紅燈時只准寫剛好讓它失敗的測試、綠燈時只准寫剛好讓它通過的程式碼、重構時不准改變行為(測試必須全程保持綠)。 三者不混做,就像 分離命令與查詢 一樣——一次只做一件事。
🔑 Takeaways
- 紅-綠-重構是用測試驅動設計的節奏,不只是「先寫測試」。
- 綠燈階段可以簡陋(fake it),先拿安全網、再於重構長成真實作。
- 步伐越小越安全;自動化重構讓你敢走大膽的一步。
- 重構階段靠測試護體,是 提取函式 等手法能安全落地的前提。
No notes yet — jot your takeaways or Q&A here.