為可測試性而設計 (Design for Testability)

★★★★★Draft

FromThe Art of Unit Testing

📚 From the Books

為可測試性而設計 (Design for Testability)Testing

一個好的單元測試要 FICC——快(Fast)、隔離(Isolated)、免設定(Configuration-free)、一致(Consistent)。如果你替某段邏輯寫測試很困難、很花時間,那不是測試的錯,而是設計不具可測試性。為可測試性而設計,就是在寫生產程式碼時就預留 seam(接縫):可以從外部注入或替換行為的切入點,讓每個 if/迴圈/switch 都能被便宜地測到。把測試當成系統的「另一種使用者」,這件事就變得自然。

🧠 Why It Matters

NOTE

靜態方法為什麼難測?因為它充當「公共共享資源」——沒有實例可注入、不能被覆寫、也無法靠繼承換掉。這正是許多 TDD 實踐者不愛 singleton 的原因。若非用 singleton 不可,也要把業務邏輯singleton 管理邏輯拆成兩個類別(SRP),讓邏輯那半邊仍可被獨立測試。

⚖️ 建立與使用綁死 vs. 依賴注入

難測 — 建構子做真工、方法內直接 new 依賴
// 建構子就去讀設定、方法內直接 new 出 logger
// 測試無法替換任何一個依賴,只能連著真實檔案系統一起跑 → 不 FICC
class OrderProcessor {
  constructor() {
    this.config = readConfigFile("/etc/app.conf"); // 建構子做真工:碰檔案系統
  }

  process(order) {
    const logger = new FileLogger("/var/log/app.log"); // 方法內 new 具體類別
    if (!this.config.enabled) {
      logger.log("processing disabled");
      return false;
    }
    // ... 真正的邏輯
    return true;
  }
}

依賴是被「直接使用」的,沒有從外面傳進來的縫。測試想控制 config 或攔截 logger,只能連真實檔案一起啟動——慢、要設定、還可能因外部原因而紅。

可測 — 建構子只接線,依賴由外注入
// 角色都抽成介面(config / logger),從建構子注入
// 建構子不做真工,只把依賴接起來 → seam 就在參數上
class OrderProcessor {
  constructor(config, logger) {
    this.config = config; // 注入,不自己讀檔
    this.logger = logger; // 注入,不自己 new
  }

  process(order) {
    if (!this.config.enabled) {
      this.logger.log("processing disabled");
      return false;
    }
    // ... 真正的邏輯
    return true;
  }
}

// 測試:塞入受控替代品,不碰檔案系統
const analyzer = new OrderProcessor(
  { enabled: false },                              // stub:餵輸入
  { calls: [], log(m) { this.calls.push(m); } },   // 需要驗互動時才用 mock
);

建立與使用被分開了:誰來組裝依賴是外面的事,OrderProcessor 只管用。測試因此能一句話換掉整個環境。

TIP

若語言/框架允許事後替換(例如把方法設為可覆寫、或用可替換的 delegate),也是一種留縫方式——測試時繼承覆寫、或換掉 delegate 就能斷開依賴。重點不在語法,而在「有沒有一個不改原始碼就能替換行為的入口」。

⚖️ 這值得嗎——本章提出的批評

為可測試性設計並非沒有代價,作者誠實地列出爭議面:

IMPORTANT

更根本的一點:在 Ruby、Smalltalk 等動態語言裡,執行期什麼都能替換,根本不太需要「為可測試性設計」。有趣的是 Ruby 社群後來仍發現了設計本身的重要性——這暗示設計是獨立於可測試性的活動。所以結論是:可測試性不該是設計目標,良好的設計才是。 通過可測試性檢驗,不代表設計就是好的;而遵循 SOLID 帶來的可維護、可讀、好開發,即使可測試性不再是問題也依然成立。

🔑 Takeaways

✍️ My Notes

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

📖 Further Reading

🔗 Dive Deeper