本章回顧整個 Money 範例,從流程到結果,涵蓋六個面向:後續工作、隱喻的力量、JUnit 使用方式、程式碼度量、開發流程,以及測試品質。
What’s Next?#
程式碼完成了嗎?沒有。 Sum.plus() 和 Money.plus() 之間仍有重複。如果將 Expression 從介面改為類別(不常見的方向,通常是類別演變為介面),就能為共用程式碼找到自然的歸屬。
作者不相信「完成」這個概念。TDD 可以用來追求完美,但那不是它最有效的用途。經常修改的核心部分應該堅若磐石,讓你能自信地進行日常變更;而系統邊緣不常改動的部分,測試可以稀疏些、設計可以醜些,不會影響你的信心。
當所有明顯的工作都做完後,可以考慮的下一步:
- 執行程式碼分析工具(如 SmallLint):自動化工具不會忘記,能指出你已知但還沒處理的問題
- 思考「不該通過」的測試:如果一個你認為會失敗的測試竟然通過了,你需要找出原因
- 清單空了就回顧設計:概念和命名是否協調?是否有難以消除的重複?殘留的重複往往是潛在設計問題的症狀
Metaphor 的力量#
作者在撰寫本書時意外地使用了 Expression(運算式) 作為隱喻,讓設計走向了全新的方向。
過去 Ward Cunningham 用 向量(vector) 來表示「多種貨幣的組合」。作者也用過 MoneySum、MoneyBag、Wallet 等名稱。這些隱喻都暗示集合是扁平的——例如 “2 USD + 5 CHF + 3 USD” 會合併為 “5 USD + 5 CHF”。
而 Expression 隱喻讓作者擺脫了合併重複貨幣的麻煩問題,程式碼比過去任何一次都更乾淨、更清楚。雖然對 Expression 的效能有些擔憂,但作者傾向等到有實際使用數據後再優化。
重點: 隱喻不只是取名的來源,它深刻地影響設計的結構方向。選擇不同的隱喻,可能導向截然不同的設計。
JUnit 使用方式#
作者在撰寫 Money 範例時讓 JUnit 記錄了操作日誌:
- 總共按了 125 次 Run 按鈕
- 在純寫程式的時段,大約每分鐘執行一次測試
- 整個過程中只有一次對結果感到意外,那次是在匆忙中做的重構

Figure 17.1: Histogram of the time interval between test runs
長間隔的大量出現很可能是因為同時在寫書。
Code Metrics#
| 指標 | 正式程式碼 | 測試程式碼 |
|---|---|---|
| 類別數 | 5 | 1 |
| 函式數 | 22 | 15 |
| 行數 | 91 | 89 |
| Cyclomatic Complexity | 1.04 | 1 |
| 行數/函式 | 4.1 | 5.9 |
幾個值得注意的觀察:
- 測試程式碼和正式程式碼的行數幾乎相同(89 vs 91)
- 測試的 cyclomatic complexity 為 1,因為測試中沒有分支或迴圈
- 正式程式碼的 complexity 也很低(1.04),因為大量使用多型來取代顯式的控制流程
- 測試中每個函式行數偏高(5.9),是因為尚未抽取共用的 fixture 建立程式碼
Process#
TDD 的循環:
- 新增一個小測試
- 執行所有測試,看到失敗
- 做一個修改
- 執行測試,看到通過
- 重構以消除重複
flowchart LR
A["1. 新增小測試"] --> B["2. 執行全部<br/>看到失敗 🔴"]
B --> C["3. 做一個修改"]
C --> D["4. 執行全部<br/>看到通過 🟢"]
D --> E["5. 重構<br/>消除重複"]
E --> A假設寫測試算一步,那每個測試需要多少次變更才能完成編譯、執行、重構的全部流程?

Figure 17.2: Number of changes per refactoring
作者預期在大型專案中,讓測試編譯並通過所需的變更數量會維持在很小的範圍。但重構所需的變更數量可能呈現「肥尾」(fat tail / leptokurtotic)分佈——類似鐘形曲線,但有更多極端值,如同股市的價格變動。
Test Quality#
TDD 自然產出的測試足以在系統運行期間持續使用,但不能取代其他類型的測試:
- 效能測試(Performance)
- 壓力測試(Stress)
- 可用性測試(Usability)
語句覆蓋率(Statement Coverage)#
嚴格遵循 TDD 應該能達到 100% 語句覆蓋率。JProbe 報告只有一行未被測試覆蓋——Money.toString(),那是特意加入的除錯輔助方法。
缺陷插入(Defect Insertion)#
另一種評估測試品質的方式:改變一行程式碼的意義,應該要有測試因此失敗。Jester 工具報告只有一行能在不破壞測試的情況下被修改——Pair.hashCode()。因為我們的實作本來就回傳固定值 0,換成其他常數並不會改變程式的語義。
提升覆蓋率的兩種途徑#
覆蓋率的粗略度量 = 測試涵蓋的面向 / 需要測試的面向(邏輯複雜度)。
- 寫更多測試:專業測試人員為同一個問題可能寫出 65 個測試,而 TDD 開發者只寫 6 個
- 簡化程式碼邏輯:重構步驟經常有這個效果——用多型取代條件判斷,甚至完全消除分支
技巧: 用 Phlip 的話說:「與其增加測試來走遍所有輸入排列組合,不如讓同樣的測試覆蓋不斷縮小的程式碼排列組合。」
One Last Review#
學習 TDD 時反覆出現的三個驚喜:
- 讓測試通過的三種方法:Fake It(偽造)、Triangulation(三角測量)、Obvious Implementation(顯而易見的實作)
- 消除測試與程式碼之間的重複來驅動設計
- 控制測試之間的步伐大小:路滑時放慢腳步增加牽引力,路況好時加速巡航