什麼是單元測試 (What Is a Unit Test)

★★★★★Draft

FromThe Art of Unit Testing

📚 From the Books

什麼是單元測試 (What Is a Unit Test)Testing

「一個方法呼叫另一段程式碼、檢查假設對不對」——這是 1970 年代傳下來的單元測試定義,但它太粗糙了。真正精準的問法是:你測的不是一個方法,而是一個工作單元(unit of work)——從某個公開方法被呼叫(入口),到一個可觀察的結束結果產生(出口)為止的所有動作。搞清楚入口與出口,你才知道自己到底在測什麼;而「隔離、可控、快、可信賴」這幾個特質,正是把它跟整合測試區分開來的分水嶺。

🧠 Why It Matters

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

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading