核心觀點#
本章探討的不只是測試的驗證功能,而是撰寫測試對於軟體設計與架構產生的深遠影響。作者引用 Seneca 的名言開篇,指出撰寫單元測試的行為本身更像是一種設計行為,而非單純的驗證行為。
重點: 撰寫單元測試關閉了許多回饋迴路(feedback loops),其中功能驗證反而是最不重要的一個。測試對設計的影響遠大於對驗證的貢獻。
Test-Driven Development#
TDD 遵循三條簡單規則:
- 不寫任何 production code,除非已有一個失敗的 unit test
- 不寫超過剛好足以失敗(或無法編譯)的 unit test
- 不寫超過剛好足以讓失敗測試通過的 production code
以這種方式工作,開發者會在極短的循環內交替進行——每一兩分鐘就在寫測試和寫程式碼之間切換。
flowchart LR
A["🔴 寫一個<br/>失敗的測試"] --> B["🟢 寫剛好足以<br/>通過的程式碼"]
B --> C["🔵 重構<br/>改善結構"]
C --> ATDD 帶來的四大效益#
- 驗證的安全網:每個功能都有對應的測試,修改程式碼時不怕破壞既有功能,讓開發者更自由地改進系統
- 迫使採用呼叫者視角:先寫測試迫使我們從程式的使用者角度出發,關注介面(interface)而非實作。這讓軟體被設計為 conveniently callable(方便呼叫的)
- 迫使解耦合:為了讓軟體可測試,它必須與周遭環境解耦。先寫測試自然會 force us to decouple the software
- 活的文件:測試本身就是最好的範例文件,可編譯、可執行、永遠保持最新,不會說謊
Test-First Design 範例:Hunt the Wumpus#
作者以 Hunt the Wumpus 遊戲為例,展示了 intentional programming(意圖式程式設計)的概念:
[Test]
public void TestMove()
{
WumpusGame g = new WumpusGame();
g.Connect(4,5,"E");
g.GetPlayerRoom(4);
g.East();
Assert.AreEqual(5, g.GetPlayerRoom());
}- 先在測試中表達你的意圖,讓它盡可能簡單易讀
- 相信自己能寫出符合測試所暗示之結構的程式碼
- 測試揭示了一個有趣的設計決策:不需要
Room類別,連結(connection)的概念比「房間」更核心
技巧: 先寫測試是一種辨別設計決策的行為(The act of writing tests first is an act of discerning between design decisions)。它在非常早期就照亮了核心設計議題。
Test Isolation 與 Mock Object#
先寫測試常常揭露軟體中應該被解耦的地方。以薪資系統(Payroll)為例:

Figure 4.1: Coupled payroll model
原始設計中 Payroll 類別直接依賴 EmployeeDatabase、CheckWriter 和 Employee,這使得測試變得困難:
- 需要真實的資料庫
- 無法自動化驗證印出的支票
解決方案是使用 Mock Object 模式:在所有協作者之間插入介面,並建立實作這些介面的測試替身。

Figure 4.2: Decoupled Payroll using MOCK OBJECTS for testing
[Test]
public void TestPayroll()
{
MockEmployeeDatabase db = new MockEmployeeDatabase();
MockCheckWriter w = new MockCheckWriter();
Payroll p = new Payroll(db, w);
p.PayEmployees();
Assert.IsTrue(w.ChecksWereWrittenCorrectly());
Assert.IsTrue(db.PaymentsWerePostedCorrectly());
}Serendipitous Decoupling(意外的解耦)#
這種為了測試而進行的解耦,同時帶來了架構上的好處——允許替換不同的資料庫和支票列印器。
重點: Writing tests before code improves our designs. 為了隔離被測模組而進行的解耦,會以有益於整體程式結構的方式迫使我們解耦。
Acceptance Tests#
Unit test 是白箱測試(white box test),驗證系統的個別機制。Acceptance test 是黑箱測試(black box test),驗證客戶需求是否被滿足。
Acceptance test 的關鍵特徵:
- 由不了解系統內部機制的人撰寫(客戶、業務分析師、QA)
- 使用非技術人員也能讀寫的特殊規格語言
- 自動化執行
- 是功能的終極文件——the acceptance tests become the true requirements document
注意: 在專案早期用手動方式執行 acceptance test 是不明智的,因為這會喪失自動化測試所帶來的解耦壓力。從第一個 iteration 就應該自動化 acceptance test。
作者使用開源工具 FitNesse 撰寫 acceptance test,每個測試都是一個簡單的網頁,可從瀏覽器存取和執行。
Serendipitous Architecture(意外的架構)#
Acceptance test 對系統架構施加了壓力。以薪資系統的第一個 iteration 為例:

Figure 4.3: Sample acceptance test
考慮 acceptance test 的需求,自然導向了好的架構決策:
- 系統需要一個 API 供 FitNesse 呼叫
- UI 必須與 business rules 解耦
- 支票列印必須與
Create Paychecks功能解耦
補充: 正如 unit test 在小範圍(in the small)驅動出好的設計決策,acceptance test 在大範圍(in the large)驅動出好的架構決策。
結論#
- 測試越簡單、執行越頻繁,偏差就越快被發現
- 系統一旦運作到某個水準,就永遠不會倒退
- 驗證只是撰寫測試的好處之一;unit test 和 acceptance test 都是可編譯、可執行的文件
- 對架構和設計的影響可能是最重要的好處:越可測試的東西就越解耦,考慮全面測試的行為對軟體結構有深遠的正面影響