「一個方法呼叫另一段程式碼、檢查假設對不對」——這是 1970 年代傳下來的單元測試定義,但它太粗糙了。真正精準的問法是:你測的不是一個方法,而是一個工作單元(unit of work)——從某個公開方法被呼叫(入口),到一個可觀察的結束結果產生(出口)為止的所有動作。搞清楚入口與出口,你才知道自己到底在測什麼;而「隔離、可控、快、可信賴」這幾個特質,正是把它跟整合測試區分開來的分水嶺。
🧠 Why It Matters
- 從方法到工作單元:舊定義裡的 unit 指一個方法,作者認為不夠精確。真正的 unit 是一個 unit of work——從呼叫系統中某個公開方法(入口點)開始,到產生一個可觀察的結束結果(出口點)為止的所有動作總和。它可以小到一個方法,也可以大到橫跨多個類別。
- 只有三種出口(結束結果):一個工作單元可觀察的結束結果只有三種——回傳值(公開方法回傳非 void 的值)、狀態改變(呼叫前後系統行為變了,且不必探查私有狀態就能確認)、第三方呼叫(對測試無法控制、又不回傳值的外部系統發出呼叫,例如第三方 logging)。寫測試就是選定其中一個出口、對它的假設下斷言。
- 更新後的定義:單元測試是一段程式碼,它呼叫一個工作單元,並檢查該單元某一個特定的結束結果;假設被證明為錯,測試就失敗。它使用測試框架撰寫,容易寫、跑得快、值得信賴、可讀且可維護——只要產品程式碼沒變,結果就一致。
- 沒有邏輯的程式碼不必特別測:getter/setter 這類沒有控制流程的程式碼不需要專門為它寫測試,但它們會被工作單元用到,自然被涵蓋。真正該測的是控制流程程式碼——
if、迴圈、switch、計算,任何有決策邏輯的地方。
NOTE
這一頁只談「什麼是單元測試」。至於一個測試內部怎麼組織(Arrange-Act-Assert)、以及 FIRST 那幾個字母,交給 乾淨的測試:Test-First 與 AAA;「什麼時候寫測試」的紅綠重構節奏在 TDD 節奏;出口是「第三方呼叫」時要用的替身機制在 測試替身。
⚖️ 一個真的單元測試 vs. 一個偽裝的整合測試
作者給了一組自我檢視問題:只要其中任一題答「否」,你寫的多半不是單元測試,而是整合測試——能兩週後重跑、任何人一鍵執行、幾分鐘內全部跑完、幾分鐘內寫出一個基本測試。好的單元測試具備:自動化且可重複、容易實作、明天仍有意義、任何人一鍵執行、速度快、結果一致、完全控制被測單元、完全隔離、失敗時能清楚指出問題。
✅ 單元測試 — 隔離、可控、結果一致
// 工作單元:ParseAndSum。出口點 = 回傳值。
// 沒有真實依賴,純記憶體運算 → 每次結果都一樣、毫秒級、任何人都能跑。
test("empty string returns 0", () => {
const parser = new SimpleParser();
const result = parser.parseAndSum("");
expect(result).toBe(0); // 對「回傳值」這個出口下斷言
});工作單元的入口是公開方法 parseAndSum,出口是它的回傳值。沒碰檔案、時間或網路,所以它可重複、快速、值得信賴。
⚠️ 偽裝的整合測試 — 用了真實依賴
// 同一段邏輯,卻讓它依賴真實時間與真實檔案系統。
// 這就不再是單元測試了——結果會隨時間與環境而變。
test("today's report is summed", () => {
const parser = new SimpleParser();
const raw = fs.readFileSync(`/data/${new Date().toISOString()}.txt`); // 真實檔案 + 真實時間
const result = parser.parseAndSum(raw.toString());
expect(result).toBe(0); // 換一天、換一台機器就可能變紅
});用了系統時間、真實檔案系統——不夠快、不夠一致、不在你的控制下。這正是整合測試的定義:在沒有完全控制所有依賴的情況下測試工作單元。
IMPORTANT
整合測試使用真實依賴:系統時間、真實檔案系統、真實資料庫、網路、執行緒、亂數產生器等。單元測試則把工作單元與這些依賴隔離,使結果一致且完全可控。兩者都有價值,但要分開管理——別讓慢而不穩的整合測試混進你期待「秒回、全綠」的單元測試套件裡。
整合測試的代價很具體:無法隨時重跑(改了程式卻不能立刻驗證,容易「意外引入 bug」)、需要特定環境設定別人跑不動、用真實資源所以慢、依賴 DateTime.Now 之類的東西所以結果不一致、要處理所有內外部依賴所以難寫、一次測太多東西所以失敗時難定位——就像汽車拋錨很難判斷是哪個子系統壞了。
WARNING
寫一個糟糕的單元測試毫無意義。若不自覺地寫出品質差的測試,不如不寫,以免日後在可維護性與時程上付出更大代價。順帶一提,Michael Feathers 對 legacy code 的定義就是「沒有測試的程式碼」——沒有測試保護,改動就像在未知狀態中冒險。
🔑 Takeaways
- 單元測試測的不是一個方法,而是一個工作單元:從公開方法入口,到可觀察結束結果出口的所有動作。
- 出口只有三種:回傳值、狀態改變、第三方呼叫;一個測試選定一個出口下斷言。
- 好的單元測試 = 自動化、可重複、快、隔離、可控、結果一致、失敗訊息清楚。
- 單元測試隔離依賴、整合測試用真實依賴;兩者價值不同,必須分開管理。
- 該測的是有邏輯的控制流程程式碼;getter/setter 這類無邏輯程式碼會被自然涵蓋,不必專測。
No notes yet — jot your takeaways or Q&A here.