本章是全書的起點,從「什麼是單元測試」這個最基本的問題出發,逐步建立起一個精確的定義,並將單元測試與整合測試做出清楚的區隔。最後介紹測試驅動開發(TDD)的概念,以及成功實踐 TDD 所需的三項核心技能。
逐步定義單元測試#
傳統定義#
單元測試的概念可以追溯到 1970 年代的 Smalltalk 語言,由 Kent Beck 引入並推廣至各種程式語言。維基百科給出的經典定義是:
單元測試是一段程式碼(通常是一個方法),它呼叫另一段程式碼,然後檢查某些假設是否正確。如果假設被證明是錯誤的,測試就失敗了。
在這個定義中,unit 指的是一個方法或函式。但作者認為這個定義已經不夠精確。
Unit of Work(工作單元)#
作者重新定義了 unit 的含義——它不再僅僅是一個方法,而是一個 unit of work(工作單元)。
Unit of Work 是從呼叫系統中某個公開方法開始,到產生一個可觀察的結束結果為止的所有動作總和。工作單元的範圍可以小到一個方法,也可以大到橫跨多個類別。
工作單元有三種可觀察的結束結果:
- 回傳值:被呼叫的公開方法回傳一個非 void 的值
- 狀態改變:系統在呼叫前後產生了可觀察的行為或狀態變化,且不需要探查私有狀態就能確認(例如:使用者登入後系統行為改變)
- 第三方呼叫:對測試無法控制的外部系統發出呼叫,且該外部系統不回傳任何值(例如:呼叫第三方 logging 系統)
mindmap
root((Unit of Work 結束結果))
回傳值
公開方法回傳非 void 的值
狀態改變
系統產生可觀察的行為變化
第三方呼叫
對外部系統發出呼叫更新後的定義#
根據工作單元的概念,作者給出了演進後的定義:
單元測試是一段程式碼,它呼叫一個工作單元,並檢查該工作單元的某一個特定結束結果。如果對結束結果的假設被證明是錯的,測試就失敗了。單元測試的範圍可以小到一個方法,也可以大到多個類別。
好的單元測試的特質#
在定義「好的」單元測試之前,作者提出了一系列自我檢視問題。如果你對以下任何一題的答案是「否」,那你寫的可能不是單元測試,而是整合測試:
- 能否在兩週或數月後重新執行測試並取得結果?
- 團隊中的任何成員都能執行我寫的測試嗎?
- 能否在幾分鐘內跑完所有單元測試?
- 能否一鍵執行所有單元測試?
- 能否在幾分鐘內寫出一個基本的測試?
好的單元測試應該具備以下特質:
- 自動化且可重複執行
- 容易實作
- 明天仍然有意義(不會因時間改變而失效)
- 任何人都能一鍵執行
- 執行速度快
- 結果一致(相同條件下每次結果相同)
- 完全控制被測試的單元
- 完全隔離(獨立於其他測試執行)
- 失敗時能清楚指出問題所在

Figure 1.1: In classic testing, developers use a GUI to trigger actions on the class they want to test
整合測試#
什麼是整合測試#
作者將整合測試定義為:凡是不夠快速、不夠一致,且使用了被測試單元的一個或多個真實依賴的測試。
整合測試是在沒有完全控制所有依賴的情況下測試一個工作單元,它使用了真實的依賴項目,例如系統時間、真實檔案系統、真實資料庫、網路、執行緒、亂數產生器等。
簡單來說:整合測試使用真實依賴;單元測試則隔離工作單元與其依賴,使測試結果一致且可完全控制。
整合測試的缺點#

Figure 1.2: You can have many failure points in an integration test
將 1.2 節的自我檢視問題套用到整合測試上,可以看出幾個明顯問題:
| 問題 | 說明 |
|---|---|
| 無法隨時重新執行 | 程式碼不斷變動,若無法在修改後立即執行測試,就可能在不知情的情況下破壞既有功能。作者稱之為「意外引入的 bug」(accidental bugging) |
| 無法讓團隊成員輕易執行 | 需要特定環境設定(如資料庫連線字串),其他人不容易直接跑測試 |
| 執行速度慢 | 使用真實資料庫或外部資源,速度遠比記憶體中的操作慢。當測試數量達到數百個時,每半秒都很關鍵 |
| 結果不一致 | 例如測試依賴系統時間 DateTime.Now,每次執行的時間都不同,測試本質上就不一致 |
| 難以撰寫 | 需要處理所有內部和外部依賴,撰寫和維護成本高 |
| 測試太多東西 | 一次測試多個元件,失敗時難以定位問題根源,就像汽車拋錨時很難判斷是哪個子系統出問題 |
寫一個糟糕的單元測試毫無意義。如果你不自覺地寫出品質差的測試,不如不寫,以免日後在可維護性和時程上付出更大的代價。
Legacy Code(遺留程式碼)#
作者引用了 Michael Feathers 在 Working Effectively with Legacy Code 中的定義:
Legacy code 就是「沒有測試的程式碼」。
這個定義在閱讀本書時值得牢記——沒有測試保護的程式碼,修改時就像在未知的狀態中冒險。
什麼構成好的單元測試#
綜合以上討論,作者給出了單元測試的最終定義:
單元測試是一段自動化的程式碼,它呼叫被測試的工作單元,然後檢查該單元某個結束結果的假設。單元測試幾乎都使用單元測試框架來撰寫。它容易撰寫、執行快速、值得信賴、可讀且可維護。只要產品程式碼沒有改變,測試結果就是一致的。
作者也修正了前一版書中「只針對控制流程程式碼測試」的觀點。沒有邏輯的程式碼(如 getter/setter)雖然不需要特別針對它寫測試,但它們會被工作單元使用,所以自然會被涵蓋到。
控制流程程式碼(Control flow code)是指包含邏輯的程式碼:
if陳述式、迴圈、switch/case、計算、或任何決策邏輯。
簡單的單元測試範例#
作者用一個不使用任何測試框架的範例,展示了最原始的單元測試寫法。假設有一個 SimpleParser 類別:
public class SimpleParser
{
public int ParseAndSum(string numbers)
{
if (numbers.Length == 0)
{
return 0;
}
if (!numbers.Contains(","))
{
return int.Parse(numbers);
}
else
{
throw new InvalidOperationException(
"I can only handle 0 or 1 numbers for now!");
}
}
}最簡單的測試方式是寫一個 console 應用程式:
class SimpleParserTests
{
public static void TestReturnsZeroWhenEmptyString()
{
try
{
SimpleParser p = new SimpleParser();
int result = p.ParseAndSum(string.Empty);
if (result != 0)
{
Console.WriteLine(
@"***SimpleParserTests.TestReturnsZeroWhenEmptyString:
Parse and sum should have returned 0 on an empty string");
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
Figure 1.3: The traditional way of writing unit tests
這種方式雖然可行,但非常 ad hoc。隨著測試數量增加,你會需要通用的 helper 方法來格式化錯誤訊息、處理 null 檢查等。這正是單元測試框架存在的意義——它們提供了這些基礎設施,讓測試更容易撰寫和維護(第 2 章將介紹如何使用框架)。
測試驅動開發(TDD)#
當你掌握了如何用測試框架撰寫結構化、可維護的測試後,下一個問題是:什麼時候寫測試?
傳統做法是在程式寫完之後補測試,但越來越多人選擇在撰寫產品程式碼之前先寫測試。這種方法稱為 Test-Driven Development(TDD),或稱 test-first development。
TDD 有許多不同的解讀。有人認為它是 test-first development,有人認為它是一種設計方法。在本書中,TDD 指的是 test-first development,設計在其中扮演次要角色。
TDD 的三個步驟#
- 撰寫一個失敗的測試:測試撰寫時假設產品程式碼已經完成,因此測試會失敗(證明缺少功能或存在 bug)
- 撰寫讓測試通過的產品程式碼:產品程式碼應盡可能簡單
- 重構程式碼:測試通過後,可以重構以提升可讀性、移除重複,然後繼續下一個測試
flowchart LR
A["1. 撰寫<br/>失敗的測試"] --> B["2. 撰寫<br/>通過的程式碼"]
B --> C[3. 重構]
C --> A
Figure 1.4: Test-driven development — a bird's-eye view
TDD 的價值與風險#
正確執行 TDD 可以帶來顯著好處:
- 提升程式碼品質
- 減少 bug 數量
- 提高對程式碼的信心
- 縮短找到 bug 的時間
- 改善程式設計
- 「測試你的測試」——先看測試失敗再看它通過,等於驗證了測試本身的正確性
TDD 是一把雙面刃。執行不當可能導致專案延遲、浪費時間、降低動力和程式碼品質。TDD 本身不保證專案成功或測試的健壯性與可維護性。
TDD 成功的三項核心技能#
成功的 TDD 需要三種獨立的技能,作者建議一次專注學習一種,而非同時學習:
- 寫好測試的能力:知道如何撰寫可信賴、可讀、可維護的測試。這正是本書的核心主題
- Test-first 的能力:在產品程式碼之前先寫測試。這是大多數 TDD 書籍教的內容,但光會 test-first 不代表測試品質好。推薦 Kent Beck 的 Test-Driven Development: by Example
- 良好的設計能力:即使測試可讀且可維護,也不代表最終會得到好的系統設計。設計技能需要另外培養。推薦 Growing Object-Oriented Software, Guided by Tests 和 Clean Code
mindmap
root((TDD 三項核心技能))
寫好測試的能力
可信賴、可讀、可維護
本書核心主題
Test-first 的能力
先寫測試再寫程式碼
良好的設計能力
系統架構設計
需另外培養許多人試圖同時學習這三種技能,結果因為難度過高而放棄。建議採取漸進式學習——一次專注一種技能,降低挫折感,提高成功機率。
總結#
本章建立的核心概念:
- 單元測試是一段自動化程式碼,呼叫工作單元並檢查某個結束結果,使用測試框架撰寫,容易寫、跑得快、結果一致、值得信賴
- 整合測試使用真實依賴,難以撰寫和自動化、執行慢、需要環境設定。雖然整合測試也有其價值,但應與單元測試分開管理
- TDD 是先寫測試再寫產品程式碼的開發方式,能確保高程式碼覆蓋率並「測試你的測試」,但需要正確執行才能發揮效益
- 成功的 TDD 需要三項獨立技能:好的測試、test-first、好的設計,建議逐一學習