概述#
本章深入探討 TDD 的進階主題:從演算法的漸進式推導(Sort 範例),到卡住時如何脫困(Word Wrap 範例),再到測試的基本模式(Arrange/Act/Assert 與 BDD),最後介紹 Test Doubles 的五種型態以及 TDD 不確定性原理(TDD Uncertainty Principle),並以 London 與 Chicago 兩大學派的比較收尾。
Sort 1:Bubble Sort 的漸進式推導#
作者以「排序整數陣列」為題,展示 TDD 如何一步步「逼出」演算法:
- 從最退化(degenerate)的測試開始:空陣列、單元素陣列
- 逐步加入兩個元素亂序、三個元素亂序等測試案例
- 每個測試都讓 production code 更加泛化
- 結果自然推導出 Bubble Sort——一個效能極差的排序演算法
在每一步中,測試越來越具體且具約束力,而 production code 越來越泛化。這個過程持續到你想不出任何會失敗的新測試為止。
Sort 2:Quick Sort 的漸進式推導#
同樣的排序問題,作者選擇了不同的路徑:
- 面對兩元素亂序的測試時,Sort 1 選擇「就地比較交換」(compare and swap),Sort 2 選擇「建立新的已排序列表」
- Sort 2 引入 三分法(Law of Trichotomy):選一個元素作為 pivot,將其他元素分為 lessers 和 greaters
- 對 lessers 和 greaters 遞迴呼叫 sort,最終推導出 Quick Sort
- Quick Sort 排序一百萬個隨機整數約 1.5 秒;Bubble Sort 需要約六個月
兩元素亂序這個測試存在分叉點(fork in the road)——不同的解法會導向截然不同的演算法。辨識這些分叉點並選擇正確路徑,有時至關重要。
flowchart TD
A["空陣列"] --> B["單元素"]
B --> C["兩元素已排序"]
C --> D{"兩元素亂序<br/>(分岔點)"}
D -->|"路徑 A:就地比較交換"| E["Compare & Swap"]
D -->|"路徑 B:建立新的已排序列表"| F["建立 lessers / greaters"]
E --> G["Bubble Sort"]
F --> H["Quick Sort"]
style D fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#fbb,stroke:#333
style H fill:#bfb,stroke:#333作者在此過程中提出:
Rule 7: 在測試下一個更複雜的案例之前,先窮盡當前較簡單案例的所有情況。
Getting Stuck:卡住時怎麼辦#
TDD 新手常遇到的問題:寫出一個完全合理的測試,卻發現必須一次實作整個演算法才能讓它通過。作者稱之為 “getting stuck”。
Rule 8: 如果要實作太多才能讓當前測試通過,就刪掉那個測試,改寫一個更簡單的測試。
Word Wrap 範例#
- 問題:給定一個字串和寬度 N,在適當位置插入換行,盡量在單字邊界斷行
- 錯誤路徑:直接用 Gettysburg Address 的前幾個單字作為測試,很快就卡住
- 正確路徑:從最退化的測試開始——空字串、單字元、連續的
x字元(無空格)- 逐步增加複雜度:加入空格、多個單字
- 使用 遞迴 而非迴圈,讓解法自然浮現
- 最後加入
lastIndexOf(" ", w)來處理在單字邊界斷行
遞迴是一個常被忽視的解法。如果迴圈看起來很複雜,試試遞迴。
作者從此範例歸納出:
Rule 9: 遵循一個有計畫的、漸進式的模式來覆蓋測試空間。
Arrange, Act, Assert(AAA)#
Bill Wake 提出的 3A 模式,是所有測試的基本結構:
| 步驟 | 中文 | 說明 |
|---|---|---|
| Arrange | 安排 | 準備測試資料,將系統置於所需狀態 |
| Act | 執行 | 呼叫被測試的函式或動作 |
| Assert | 斷言 | 檢查輸出或系統的新狀態 |
@Test
public void gutterGame() throws Exception {
rollMany(20, 0); // Arrange
assertEquals(0, g.score()); // Act + Assert
}BDD(Behavior-Driven Development)#
- 2003 年 Dan North 提出 Given-When-Then(GWT)模式
- GWT 與 AAA 本質上是同義詞:
- Given = Arrange
- When = Act
- Then = Assert
- BDD 後來從測試延伸到系統規格描述(system specification)
有限狀態機(Finite State Machines)#
AAA / GWT 的三元組與有限狀態機的狀態轉移(transition)本質相同:
- Current State(Given/Arrange)→ Event(When/Act)→ Next State(Then/Assert)
flowchart LR
subgraph AAA["AAA 模式"]
A1["Arrange"]
A2["Act"]
A3["Assert"]
end
subgraph BDD["BDD 模式"]
B1["Given"]
B2["When"]
B3["Then"]
end
subgraph FSM["有限狀態機"]
F1["Current State"]
F2["Event"]
F3["Next State"]
end
A1 --> A2 --> A3
B1 --> B2 --> B3
F1 --> F2 --> F3
A1 -..- B1 -..- F1
A2 -..- B2 -..- F2
A3 -..- B3 -..- F3
Figure 3.1: Transition/state diagram for a subway turnstile
以地鐵旋轉門為例:
| 當前狀態 | 事件 | 下一狀態 |
|---|---|---|
| Locked | Coin | Unlocked |
| Locked | Pass | Alarming |
| Unlocked | Coin | Refunding |
| Unlocked | Pass | Locked |
你寫的每個測試都是你正在建立的有限狀態機的一個狀態轉移。 你的測試套件,如果完整的話,就是那個有限狀態機本身。
Test Doubles(測試替身)#
2000 年 Freeman、McKinnon、Craig 發表 “Endo-Testing” 論文,引入了 mock 一詞。2007 年 Gerard Meszaros 在 xUnit Test Patterns 中正式定義了五種 Test Doubles:

Figure 3.2: Test doubles
Test Doubles 的核心機制是多型(polymorphism):將外部依賴隔離在多型介面之後,再用測試替身實作該介面。
Dummy(虛設物件)#

Figure 3.3: The dummy
- 介面的空實作,所有方法都不做任何事,回傳值盡可能為 null 或 zero
- 用於:被測函式需要某個物件作為參數,但測試邏輯不需要該物件
- 目的:避免建立昂貴且不必要的真實依賴鏈
public class AuthenticatorDummy implements Authenticator {
public Boolean authenticate(String username, String password) {
return null;
}
}Rule 10: 不要在測試中包含測試不需要的東西。
Stub(殘根物件)#

Figure 3.4: The stub
- Stub 是一個 Dummy,但回傳特定值以驅動被測系統走過特定路徑
- 例如
RejectingAuthenticator永遠回傳false,PromiscuousAuthenticator永遠回傳true
Rule 11: 不要在測試中使用生產資料。
Spy(間諜物件)#

Figure 3.5: The spy
- Spy 是一個 Stub,額外記住對它做了什麼,允許測試事後查詢
- 可以記錄:呼叫次數、傳入的參數等
- Spy 可簡單如一個 boolean flag,也可複雜到維護完整的呼叫歷史
- 危險之處:Spy 將測試耦合到被測函式的實作細節
AuthenticatorSpy spy = new AuthenticatorSpy();
// ... 執行測試 ...
assertEquals(1, spy.getCount());
assertEquals("user", spy.getLastUsername());Mock(模擬物件)#

Figure 3.6: The mock object
- Mock 是一個 Spy,額外知道預期行為並能自行判斷測試是否通過
- 測試斷言被寫進 Mock 本身
- Mock 可以變得非常複雜,複雜到需要為 Mock 本身寫測試
- 作者個人不喜歡 Mock,因為它將 spy 行為與測試斷言耦合在一起
Fake(偽造物件)#

Figure 3.7: The fake
- Fake 不是 Dummy/Stub/Spy/Mock,而是一種模擬器(simulator)
- 實作了簡化版的業務規則,讓測試可以控制 Fake 的行為
- 問題:隨著應用成長,Fake 傾向不斷膨脹,最終複雜到需要自己的測試
TDD 不確定性原理#
作者以計算三角函數 sin(x) 為極端範例,揭示 TDD 的根本限制。
問題的兩面#
第一面:不確定性
- 使用 value tests(輸入值對應輸出值)測試
sin(x),無論測多少值,始終無法確定是否遺漏了某個會失敗的輸入 - 要窮舉所有 double 值需要約 2 x 10^19 個測試
第二面:脆弱性
- 使用 Spy 深入檢查 Taylor 級數的計算過程,可以獲得更高的確定性
- 但如果改用其他演算法(如 CORDIC),所有 spy 測試都會壞掉
- 而 value tests 不受演算法更換影響
TDD 不確定性原理:你對測試要求的確定性越高,測試就越不靈活;你對測試要求的靈活性越高,確定性就越低。
London vs. Chicago 學派#
| 面向 | London 學派 | Chicago 學派 |
|---|---|---|
| 代表人物 | Steve Freeman、Nat Pryce(著作:Growing Object-Oriented Software, Guided by Tests) | Martin Fowler / ThoughtWorks(又稱 Detroit 學派) |
| 偏好 | 確定性勝過靈活性 | 靈活性勝過確定性 |
| 測試工具 | 大量使用 mocks 和 spies | 較少使用 mocks |
| 關注重點 | 演算法而非結果 | 結果而非互動和演算法 |
| 設計方向 | Outside-In:從 UI 設計到業務規則,一次一個 use case | Inside-Out:從業務規則開始,逐步向外擴展到 UI,在各層之間尋找設計模式和抽象機會 |
綜合(Synthesis)#
- 兩個學派並非對立,只是側重不同
- 所有實踐者都會混合使用兩種技術
- 作者傾向 Chicago 學派,但認為兩者的融合才是最佳實踐
作者的架構取捨#

Figure 3.8: Architecture diagram example
- 跨越架構邊界的測試:使用 Spy(London 風格),確保元件正確呼叫協作者
- 元件內部的測試:使用 value 和 property tests(Chicago 風格),降低耦合和脆弱性
- 具體範例:
- Business Objects:可能用些 Stub,不需要 Spy/Mock(不知道其他元件)
- Interactors:用 Spy 確保正確操作 DB 和 GUI
- Controller:幾乎一定用 Spy 代替 Interactor
- Presenter:需要 Spy 代替真實 View
測試的需求常常會影響架構邊界的劃分。不要害怕因為測試的需要而調整元件邊界。
本章提出的規則總整理#
| 規則 | 內容 |
|---|---|
| Rule 7 | 在測試下一個更複雜的案例之前,先窮盡當前較簡單案例 |
| Rule 8 | 如果要實作太多才能讓測試通過,刪掉它,寫一個更簡單的測試 |
| Rule 9 | 遵循有計畫的、漸進式的模式來覆蓋測試空間 |
| Rule 10 | 不要在測試中包含測試不需要的東西 |
| Rule 11 | 不要在測試中使用生產資料 |