Dependency-Breaking Techniques#
本章是一份 dependency-breaking techniques 的完整目錄,共包含 24 種技巧。每種技巧都旨在幫助你打破 legacy code 中的依賴,讓程式碼能夠被放入 test harness 中進行測試。
1. Adapt Parameter#
當你無法在測試中使用某個方法的參數型別時——可能因為該型別是由 vendor 提供、建構成本太高,或依賴了難以在測試中重現的基礎設施——你可以建立一個 wrapper 來適配它。
適用時機:
- 參數型別是你無法輕易在測試中建立的(例如 framework 或 vendor 提供的物件)
- 你無法使用 Extract Interface 因為你不擁有該型別
步驟:
- 建立新的 interface,使其包含你在方法中需要使用的參數方法
- 建立一個 adapter class 實作該 interface,並持有原始參數的實例,將呼叫委派給它
- 建立一個 fake class 也實作該 interface,用於測試
- 修改方法的參數型別為新的 interface
2. Break Out Method Object#
當你有一個非常長的方法且無法輕易提取部分程式碼時,可以將整個方法搬到一個新的 class 中。方法的參數變成 constructor 的參數,方法的本體進入新 class 的一個名為 run 或 execute 的方法中。
適用時機:
- 方法很長且難以測試
- 你想把方法中的臨時變數變成 instance variables 以便 sensing
- 原始 class 的依賴使其難以實例化
步驟:
- 建立一個新 class,名稱基於原始方法的用途
- 為原始方法的每個參數以及原始物件的參考建立 constructor 參數。使用 Preserve Signatures 來降低風險
- 為原始方法中使用的每個 local variable 和 temporary variable 建立 instance variable
- 建立一個
run方法,將原始方法的本體複製進去 - 在原始方法中建立新 class 的實例並呼叫
run

Figure 25.1: GDIBrush after Break Out Method Object
3. Definition Completion#
在 C 和 C++ 中,你可以將型別的宣告(declaration)和定義(definition)分開。你可以利用這一點,在測試檔案中提供替代的定義。
適用時機:
- 在 C/C++ 中,你需要替換某個型別的具體實作
- 你想在測試中提供不同的行為
步驟:
- 確認你想替換的型別的宣告
- 在測試檔案中提供該型別的替代定義
- 確保 build 設定讓測試使用替代定義而非原始定義
4. Encapsulate Global References#
當你有多組全域變數或全域函式被一組方法使用時,可以將它們包進一個 class 中。這讓你可以使用 Subclass and Override Method 等技巧來替換它們。
適用時機:
- 全域變數或函式使得程式碼難以測試
- 多個全域項目在概念上屬於同一個群組
步驟:
- 確認你想封裝的全域變數和函式
- 建立一個 class,把它們作為 instance variable 和 method
- 如果全域變數只有一組合理的值,考慮使用 Singleton(但要讓它可被測試替換)
- 將所有對全域變數的引用改為對新 class 實例的引用
- 使用 Lean on the Compiler 來找到所有需要修改的地方
5. Expose Static Method#
如果你有一個方法不使用任何 instance data,你可以把它變成 static。這樣在測試中就不需要實例化整個 class,可以直接呼叫 static method。
適用時機:
- 方法不依賴任何 instance state
- 實例化所屬的 class 非常困難
步驟:
- 確認該方法不引用任何 instance variable 或 instance method
- 將方法宣告為
static - 編譯,確認沒有問題
- 在測試中直接透過類別名稱呼叫該方法
Expose Static Method 並不是理想的設計。它破壞了封裝,讓方法可以在沒有物件的情況下被呼叫。但它是一個有用的中間步驟——先讓程式碼可測試,之後再改善設計。
6. Extract and Override Call#
當你的方法中有一個對其他物件的呼叫導致了依賴問題,你可以將該呼叫提取到自己的方法中,然後在測試用的 subclass 中 override 它。
適用時機:
- 方法中有一個特定的呼叫(例如對外部 API、資料庫等)需要在測試中替換
- 你想快速建立 seam
步驟:
- 找到你想替換的呼叫
- 將該呼叫提取成一個新方法
- 在測試中建立一個 subclass,override 該新方法以提供 fake 行為
7. Extract and Override Factory Method#
如果在 constructor 中建立了難以測試的物件,你可以將物件建立提取到 factory method 中,然後在測試用的 subclass 中 override 它。
適用時機:
- Constructor 中有
new表達式建立了難以在測試中使用的物件 - 你想替換 constructor 中建立的物件但不想修改 constructor 簽名
步驟:
- 將物件建立提取成一個 factory method
- 在測試中建立 subclass,override 該 factory method 以回傳 fake 物件
在 C++ 中要小心在 constructor 中呼叫 virtual method——C++ 不會 dispatch 到 derived class 的 override。Java 和 C# 則可以正常運作。
8. Extract and Override Getter#
類似 Extract and Override Factory Method,但使用 lazy initialization 的 getter。不在 constructor 中建立物件,而是在第一次被需要時透過 getter 建立。
適用時機:
- 你想避免在 constructor 中建立物件的問題
- 語言不支援在 constructor 中呼叫 virtual method(如 C++)
步驟:
- 找到你想替換的 instance variable
- 建立一個 getter method,使用 lazy initialization
- 將所有對該 variable 的直接引用改為呼叫 getter
- 建立測試用 subclass,override getter 回傳 fake 物件
作者不太喜歡 lazy initialization,因為它違反了「物件在建構完成後即處於有效狀態」的原則。建議只在確實需要時使用。
9. Extract Implementer#
當你有一個 class 你想 mock,但它不實作任何 interface 且直接在很多地方被使用時,你可以將它的實作推到一個新 class 中,讓原始 class 變成 interface。
適用時機:
- 該 class 被很多地方直接引用,你不想到處修改型別宣告
- 你想要一個 interface 但改名代價太大
步驟:
- 將原始 class 複製為新 class(例如加上
Impl後綴) - 將原始 class 改為 interface,刪除所有方法體和 instance variable
- 讓新 class 實作原始 interface
- 編譯並修正所有需要使用具體 class 的地方(如
new呼叫)

Figure 25.2: ModelNode with superclass and subclass

Figure 25.3: After Extract Implementer on Node

Figure 25.4: Extract Implementer on ModelNode
10. Extract Interface#
建立一個 interface,包含你在測試中需要使用的方法。讓原始 class 實作該 interface,並在測試中建立 fake class 也實作該 interface。
適用時機:
- 你想 mock 一個 class 以便進行 sensing 或 separation
- 你不需要 fake class 中的所有方法,只需要其中幾個
步驟:
- 建立新的 interface,給它一個好名字
- 讓目標 class 實作該 interface,暫時不要為 interface 加入任何方法
- 修改你想測試的地方,使用 interface 型別而非具體型別
- 編譯——編譯錯誤會告訴你哪些方法需要加到 interface 中
為 interface 命名時不要只是在 class 名稱前加
I。想想這組方法的本質是什麼,給它一個有意義的概念名稱。好的命名能提升設計品質。

Figure 25.5: PaydayTransaction depending on TransactionLog
11. Introduce Instance Delegator#
當 class 上有 static method 導致測試困難時(因為 static cling——對 static method 的依賴無法透過 object seam 替換),你可以新增一個 instance method 來委派給 static method。
適用時機:
- 你的程式碼依賴 utility class 的 static method
- 你需要在測試中替換這些行為
步驟:
- 找到造成問題的 static method
- 在同一個 class 上建立一個 instance method,讓它委派給 static method
- 在呼叫端改為使用 instance method(需要有該 class 的實例)
- 使用 Parameterize Method 或其他技巧將實例傳入需要它的地方
隨著時間推移,當所有呼叫都透過 instance method 時,你可以把 static method 的本體搬到 instance method 中,並刪除 static 版本。
12. Introduce Static Setter#
當你有全域 mutable data(通常是 Singleton)阻礙了測試,你可以新增一個 static setter 來替換該 instance,讓測試能注入 fake 物件。
適用時機:
- 你需要替換 Singleton 或其他全域物件以進行測試
- 全域狀態使得 test harness 難以設定
步驟:
- 降低 constructor 的 protection level(改為
protected),以便 subclass - 新增一個 static setter,接受該 singleton class(或其 interface)的引用。確保 setter 會在設定新物件前正確銷毀舊 instance
- 如果需要存取 singleton 的 private/protected method 來設定測試,考慮 subclass 或 extract interface
這種技巧確實會弱化 access protection。但記住——access protection 的目的是防止錯誤,而我們在測試中做的也是防止錯誤,只是需要更強的工具。在程式碼中留下註解說明這是為了測試目的。
對於 global factory(回傳新物件而非持有實例的 static method),可以讓 factory 委派給一個可替換的 server 物件。
記住在使用 static setter 時,你修改的是所有測試共享的狀態。使用 xUnit framework 的 setUp 和 tearDown 方法來確保狀態被正確清理。
13. Link Substitution#
在程序式語言(如 C)中,物件導向的替換方式不可用。但你可以透過在連結時替換函式來達到類似效果——建立一個 dummy library,包含與你要 fake 的函式相同 signature 的函式。
適用時機:
- 你在使用 C 等程序式語言
- 你想 fake 外部函式庫的呼叫
- 最適合 fake 的函式庫是那些主要作為 data sink 的(你呼叫其中的函式,但不太關心回傳值)
步驟:
- 確認你想 fake 的函式或 class
- 提供替代的定義
- 調整 build 設定,讓測試時使用替代定義而非正式版本
Link Substitution 也可以用在 Java 中——建立具有相同名稱和方法的 class,修改 classpath 讓呼叫解析到你的替代版本。
14. Parameterize Constructor#
如果 constructor 中建立了你想替換的物件,最簡單的方式是將該物件從外部建立,作為參數傳入 constructor。
適用時機:
- Constructor 中有
new表達式你想替換 - 你想做 dependency injection
步驟:
- 找到你想參數化的 constructor 並複製一份
- 為複製版新增一個參數,用於接收你想替換的物件。移除物件建立,改為從參數賦值
- 如果語言支援 constructor 互相呼叫,讓原始 constructor 呼叫新 constructor,並在呼叫中使用原始的
new表達式
public class MailChecker {
public MailChecker(int checkPeriodSeconds) {
this(new MailReceiver(), checkPeriodSeconds);
}
public MailChecker(MailReceiver receiver, int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}
}這樣既可以在測試中傳入 fake 物件,又不影響現有的 client code。
15. Parameterize Method#
類似 Parameterize Constructor,但適用於方法。如果方法內部建立了你想替換的物件,將該物件作為參數傳入。
適用時機:
- 方法內部建立了你想在測試中替換的物件
- 你想從外部控制方法使用的物件
步驟:
- 找到你想替換的方法並複製一份
- 為複製版新增一個參數,用於接收要替換的物件,移除內部的物件建立
- 刪除原始方法的本體,改為呼叫參數化版本(傳入原始的
new表達式)
16. Primitivize Parameter#
當一個 class 因為龐大的依賴鏈而極難放入 test harness 時,你可以用原始型別(primitive)的表示方式來繞過複雜的依賴。建立一個 free function(不屬於任何 class 的函式),用原始型別的資料結構做計算,然後在原始 class 上新增一個方法來委派給它。
適用時機:
- Class 的依賴鏈太深,無法合理地放入 test harness
- 你被逼到牆角,需要先做出功能再回頭改善
步驟:
- 開發一個 free function,用原始型別表示做你需要的工作。在過程中建立一個中間表示
- 在原始 class 上新增一個方法,建構中間表示並委派給新函式
Primitivize Parameter 會讓程式碼處於不太好的狀態——它暴露了內部表示、複製了資料、產生了未測試的程式碼。它是 Sprout Class 的前身。只有在確信你之後會花時間讓 class 可測試並把函式整合回去時,才使用這個技巧。
17. Pull Up Feature#
有時你需要使用 class 上的一組方法,但 class 的依賴阻止你實例化它——而那些依賴與你想測試的方法無關。你可以把那組方法上移到一個 abstract superclass,然後在測試中 subclass 那個 superclass。
適用時機:
- 你想測試的方法群組不直接或間接引用任何「壞」依賴
- 使用 Expose Static Method 或 Break Out Method Object 不夠直接
步驟:
- 建立一個 abstract superclass
- 將你想測試的方法和它們使用的 instance variable 從原始 class 複製到 superclass
- 讓原始 class 繼承 superclass,刪除原始 class 中已移到 superclass 的方法和變數
- 在測試中建立一個 concrete subclass(提供剩餘 abstract method 的空實作),用它來測試那些方法
18. Push Down Dependency#
如果 class 中只有少數幾個依賴造成問題,你可以把那些依賴下推到一個 subclass,讓原始 class 變成 abstract class。然後在測試中建立另一個 subclass 提供 fake 實作。
適用時機:
- 只有少數方法包含難以處理的依賴
- 那些依賴出現在 class 的具體方法中
步驟:
- 找出哪些方法包含「壞」依賴
- 建立一個 subclass,將那些方法搬到 subclass 中
- 讓原始 class 中的那些方法變成 abstract
- 建立測試用 subclass,提供 fake 實作
- 在測試中使用測試 subclass
19. Replace Function with Function Pointer#
在 C 中,你可以用 function pointer 來替代直接呼叫函式,這讓你能在測試時替換函式的實作。
適用時機:
- 在 C 程式中需要替換函式行為
- 你無法使用 Link Substitution
步驟:
- 為每個你想替換的函式宣告一個 function pointer,使用相同的簽名
- 在初始化時將 function pointer 設定為指向原始函式
- 將所有對原始函式的呼叫改為透過 function pointer 呼叫
- 在測試中,將 function pointer 改為指向你的替代函式
20. Replace Global Reference with Getter#
如果 class 中有方法直接引用全域變數或 Singleton,你可以用一個可 override 的 getter method 來替代直接引用。
適用時機:
- 方法中有對全域變數或 Singleton 的直接引用
- 你想用 Subclass and Override Method 來替換它
步驟:
- 建立一個 getter method,回傳全域引用
- 將方法中所有對全域的直接引用替換為 getter 呼叫
- 在測試用 subclass 中 override getter 以回傳 fake 物件
21. Subclass and Override Method#
這是本書中最核心的 dependency-breaking technique 之一。在物件導向語言中,繼承是一個非常強大的工具。如果你有一個 class 的某些行為需要在測試中替換,你可以 subclass 它並 override 那些方法。
適用時機:
- 幾乎任何需要在測試中替換行為的場景
- 需要做 sensing 或 separation
步驟:
- 找到你想在測試中替換的方法(或先用 Extract and Override Call 等技巧提取出來)
- 讓該方法成為
virtual(如果在 C++ 中)或確保它可以被 override - 建立一個 testing subclass
- Override 該方法,提供你在測試中需要的行為
public class TestingMessageForwarder extends MessageForwarder {
@Override
protected void sendMessage(Message message) {
// 不實際發送,只記錄
sentMessages.add(message);
}
}這個技巧可以與本章中幾乎所有其他技巧搭配使用。

Figure 25.6: TestingAccount superimposed on Account
22. Supersede Instance Variable#
如果你無法在 constructor 中替換物件(例如 constructor 中的物件建立包含重要的副作用或初始化邏輯),你可以新增一個 setter 方法,在建構之後替換該 instance variable。
適用時機:
- Constructor 中的物件建立有重要的副作用,不能簡單用 Parameterize Constructor 處理
- 你需要在建構之後替換依賴
步驟:
- 確認你想替換的 instance variable
- 建立一個方法(setter)來設定該 variable 的值
- 在測試中,先正常建構物件,然後呼叫 setter 替換為 fake 物件
盡量優先使用 Parameterize Constructor 或 Extract and Override Factory Method。Supersede Instance Variable 應該是最後手段,因為容易遺忘呼叫 setter 而導致測試使用了真實物件。
23. Template Redefinition#
在支援 template 或 generics 的語言中(如 C++),你可以將依賴的型別參數化,然後在測試中提供替代型別。
適用時機:
- 你使用 C++ 等支援 template 的語言
- 你想替換 class 使用的某個型別而不需要繼承
步驟:
- 將要替換的型別改為 template 參數
- 將原始 class 改為 template class
- 在測試中用 fake 型別實例化 template
- 在 production code 中用真實型別實例化 template
24. Text Redefinition#
在動態語言中(如 Ruby、Python),你可以在執行時重新定義方法。這讓打破依賴變得非常簡單——直接在測試設定中重新定義有問題的方法即可。
適用時機:
- 你使用動態語言(Ruby、Python、JavaScript 等)
- 你想快速替換方法行為
步驟:
- 找到你想替換的方法
- 在測試檔案中,在測試執行前重新定義該方法
- 確保重新定義只影響測試環境
動態語言讓 dependency breaking 更容易,但也意味著你失去了 compiler 的安全網。確保你有足夠的測試來捕捉型別錯誤。