章節概述#
本章是 TDD 旅程的起點。作者以多幣別報表為背景需求,示範如何從一個測試清單開始,選擇最簡單的項目下手,然後以極小的步伐讓測試從紅燈變成綠燈,最後透過消除重複來完成一輪完整的 TDD 循環。
flowchart LR
A["1. 新增測試"] --> B["2. 執行<br/>看到失敗 🔴"]
B --> C["3. 做小修改"]
C --> D["4. 執行<br/>全部通過 🟢"]
D --> E["5. 重構<br/>消除重複"]
E --> A從需求到測試清單#
假設我們有一份投資報表,需要支援多幣別:
- 能夠將兩種不同幣別的金額相加,並根據匯率轉換結果
- 能夠將一個金額(股價)乘以一個數字(股數)得到總金額
作者列出了一個待辦測試清單(to-do list),用來追蹤進度:
$5 + 10 CHF = $10(匯率 2:1 時)$5 * 2 = $10
技巧: 維護一個測試清單,隨時記錄需要撰寫的測試。完成的劃掉,想到新的就加上去。這能幫助你保持專注,也能清楚知道何時算「完成」。
從測試開始,而非從物件開始#
我們需要什麼物件?這是個陷阱問題——我們不從物件開始,而是從測試開始。
撰寫測試時,我們想像操作的理想介面(perfect interface)。從最佳的 API 出發、再反推實作,比一開始就寫出複雜醜陋的「務實」程式碼更好。
乘法的測試如下:
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10, five.amount);
}補充: 這裡有 public field、side effect、用整數表示金額等問題。作者刻意先忽略這些,記錄到待辦清單中,優先讓測試通過。小步前進是核心策略。
讓測試編譯通過#
這個測試一開始連編譯都過不了,有四個編譯錯誤。作者逐一解決,每次消滅一個錯誤:
- 沒有
Dollar類別 — 建立空類別 - 沒有建構子 — 加入空建構子
- 沒有
times(int)方法 — 加入 stub 方法 - 沒有
amount欄位 — 宣告 field
class Dollar {
int amount;
Dollar(int amount) {
}
void times(int multiplier) {
}
}現在測試可以編譯並執行了——然後我們看到它失敗了。

Figure 1.1: Progress! The test fails
重點: 失敗也是進步!我們從模糊的「給我多幣別功能」,轉變為具體的「讓這個測試通過」。問題範圍大幅縮小,恐懼感也隨之降低。
用最小變更讓測試通過#
最小的修改方式——直接將 amount 硬編碼為 10:
int amount = 10;
Figure 1.2: The test runs
測試通過了,綠燈亮起。但循環尚未結束——接下來是最關鍵的第五步:消除重複。
消除重複:從常數到變數#
那個 10 其實是 5 * 2——測試中的資料和程式碼中的資料重複了。作者透過一系列小步驟逐漸消除這個重複:
第一步:顯式寫出乘法
int amount = 5 * 2;第二步:將設值移到 times() 方法
int amount;
void times(int multiplier) {
amount = 5 * 2;
}第三步:讓建構子保存 amount
Dollar(int amount) {
this.amount = amount;
}第四步:在 times() 中使用 amount 取代常數 5
void times(int multiplier) {
amount = amount * 2;
}第五步:用參數 multiplier 取代常數 2
void times(int multiplier) {
amount = amount * multiplier;
}最終版本:使用 *= 運算子
void times(int multiplier) {
amount *= multiplier;
}重點:Dependency 與 Duplication 的關係
Steve Freeman 指出,問題的本質不是重複,而是依賴(dependency)——你無法在不改變程式碼的情況下撰寫新測試。依賴是軟體開發在各個層級上的核心問題,而重複是依賴的症狀。在 TDD 中,透過消除重複來消除依賴,最大化下一個測試只需一處修改就能通過的機會。
關於步伐大小#
技巧: TDD 的重點不是必須走極小步,而是有能力走極小步。平常可以走大步,但當事情變得詭異時,你會慶幸自己能切換到小步模式。如果你能走得太小,就一定能走到恰當的大小;反過來,如果你只走大步,就永遠不知道小步是否更合適。
本章小結#
本章完成了第一輪完整的 TDD 循環,要點回顧:
- 列出測試清單,記錄已知需要的測試
- 用程式碼片段說故事,描述我們期望的操作介面
- 暫時忽略 JUnit 細節
- 用 stub 讓測試編譯通過
- 犯下醜陋的罪讓測試執行通過(硬編碼
amount = 10) - 逐步泛化程式碼,用變數取代常數
- 隨時將新發現的問題加入待辦清單,而非一次全部處理