概述#
將 class 放入 test harness 是最困難的事情之一。最常見的四個問題:
- 物件無法輕易建立
- Test harness 無法順利 build
- Constructor 有不良副作用
- Constructor 中發生重要的工作,我們需要感知它
本章透過一系列不同語言的案例,展示如何處理這些問題。
判斷 class 是否能放入 test harness 的最好方法是:直接試試看。寫一個 test case,嘗試建立物件,讓 compiler 告訴你需要什麼。
The Case of the Irritating Parameter#
情境#
一個 Java 的 CreditValidator class,constructor 需要三個參數:
public class CreditValidator {
public CreditValidator(RGHConnection connection,
CreditMaster master,
String validatorID) {
...
}
Certificate validateCustomer(Customer customer)
throws InvalidCredit {
...
}
}RGHConnection建構時會連接伺服器(耗時且不穩定)CreditMaster從檔案載入資料(相對無害)

Figure 9.1: RGHConnection
解法:Extract Interface#
RGHConnection 是 irritating parameter – 它阻礙了測試。使用 Extract Interface 抽取出只包含業務方法的 interface:
public class FakeConnection implements IRGHConnection {
public RFDIReport report;
public void connect() {}
public void disconnect() {}
public RFDIReport RFDIReportFor(int id) { return report; }
public ACTIOReport ACTIOReportFor(int customerID) { return null; }
}
Figure 9.2: RGHConnection after extracting an interface
現在可以在測試中使用 fake:
void testNoSuccess() throws Exception {
CreditMaster master = new CreditMaster("crm2.mas", true);
IRGHConnection connection = new FakeConnection();
CreditValidator validator = new CreditValidator(
connection, master, "a");
connection.report = new RFDIReport(...);
Certificate result = validator.validateCustomer(new Customer(...));
assertEquals(Certificate.VALID, result.getStatus());
}Pass Null 技巧#
如果發現某個參數在測試的方法中根本不會被使用,可以直接傳 null:
CreditValidator validator = new CreditValidator(connection, null, "a");Pass Null 只適用於測試程式碼。在生產程式碼中不要傳 null,除非別無選擇。在 Java 和 C# 中,如果 null 被使用會拋出例外,test harness 會捕捉到。但在 C 和 C++ 中,null reference 可能導致靜默的記憶體損壞。
Null Object Pattern#
在生產程式碼中,如果經常需要處理 null,考慮使用 Null Object Pattern:建立一個什麼都不做的子類別(如 NullEmployee),避免散佈 null 檢查。
其他替代方案#
- Subclass and Override Method – 如果依賴不是硬編碼在 constructor 中,可以子類別化並覆寫產生依賴的方法
The Case of the Hidden Dependency#
情境#
C++ 的 mailing_list_dispatcher class 在 constructor 中直接 new 了一個 mail_service:
mailing_list_dispatcher::mailing_list_dispatcher()
: service(new mail_service), status(MAIL_OKAY)
{
const int client_type = 12;
service->connect();
if (service->get_status() == MS_AVAILABLE) {
service->register(this, client_type, MARK_MESSAGES_OFF);
service->set_param(client_type, ML_NOBOUNCE | ML_REPEATOFF);
}
else
status = MAIL_OFFLINE;
}問題:無法在不連接郵件系統的情況下建立物件。
解法:Parameterize Constructor#
將隱藏的依賴外部化,透過參數傳入:
mailing_list_dispatcher::mailing_list_dispatcher(mail_service *service)
: status(MAIL_OKAY)
{
const int client_type = 12;
service->connect();
// ... 同上
}保留原始 constructor 的簽名,讓既有客戶端不受影響:
mailing_list_dispatcher::mailing_list_dispatcher()
{
initialize(new mail_service);
}在 C# 和 Java 中更簡潔:
public class MailingListDispatcher {
public MailingListDispatcher()
: this(new MailService())
{}
public MailingListDispatcher(MailService service) {
...
}
}Parameterize Constructor 是最容易應用的技巧之一。搭配 Extract Interface 可以讓你傳入 fake 的 service 來測試。
The Case of the Construction Blob#
情境#
Constructor 中建立了大量物件,而且物件之間互相依賴:
class WatercolorPane {
public:
WatercolorPane(Form *border, WashBrush *brush, Pattern *backdrop)
{
anteriorPanel = new Panel(border);
anteriorPanel->setBorderColor(brush->getForeColor());
backgroundPanel = new Panel(border, backdrop);
cursor = new FocusWidget(brush, backgroundPanel);
...
}
};cursor 嵌入在一堆物件建立的流程中,無法輕易替換。

Figure 9.3: SchedulingTask
解法一:Extract and Override Factory Method#
在 Java 和 C# 中可用,但在 C++ 的 constructor 中不可用(虛擬函數在 constructor 中不會呼叫衍生類別的版本)。
解法二:Supersede Instance Variable#
寫一個 setter 方法,讓我們在建構後替換物件:
void supersedeCursor(FocusWidget *newCursor) {
delete cursor;
cursor = newCursor;
}測試時:
TEST(renderBorder, WatercolorPane) {
TestingFocusWidget *widget = new TestingFocusWidget;
WatercolorPane pane(form, border, backdrop);
pane.supersedeCursor(widget);
LONGS_EQUAL(0, pane.getComponentCount());
}在 C++ 中使用 Supersede Instance Variable 時要特別小心記憶體管理。替換物件時必須正確 delete 舊物件。在其他有 GC 的語言中則安全得多。但無論如何,不應在生產程式碼中使用 superseding method。
The Case of the Irritating Global Dependency#
情境#
Facility class 的 constructor 使用了 singleton PermitRepository:
public class Facility {
public Facility(int facilityCode, String owner,
PermitNotice notice) throws PermitViolation {
Permit associatedPermit =
PermitRepository.getInstance().findAssociatedPermit(notice);
// ...
}
}Singleton 的問題:
- 不透明 (Opacity) – 看一段程式碼時無法知道它會影響什麼全域狀態
- 難以測試 – singleton 設計上只允許一個實例,但測試需要每個測試獨立

Figure 9.4: The Permit hierarchy
解法一:Introduce Static Setter#
在 singleton 上新增 setTestingInstance 方法:
public class PermitRepository {
private static PermitRepository instance = null;
protected PermitRepository() {}
public static void setTestingInstance(PermitRepository newInstance) {
instance = newInstance;
}
public static PermitRepository getInstance() {
if (instance == null) {
instance = new PermitRepository();
}
return instance;
}
}測試時:
public void setUp() {
PermitRepository repository = new PermitRepository();
// add permits to the repository
PermitRepository.setTestingInstance(repository);
}解法二:Subclass and Override Method#
建立測試用的子類別:
public class TestingPermitRepository extends PermitRepository {
private Map permits = new HashMap();
public void addAssociatedPermit(PermitNotice notice, Permit permit) {
permits.put(notice, permit);
}
public Permit findAssociatedPermit(PermitNotice notice) {
return (Permit)permits.get(notice);
}
}解法三:Extract Interface#
對 PermitRepository 使用 Extract Interface,讓所有程式碼依賴 IPermitRepository interface:
public class PermitRepository implements IPermitRepository {
private static IPermitRepository instance = null;
// ...
}
Figure 9.5: Permit hierarchy with extract interfaces
關於放鬆 Singleton 限制#
人們使用 singleton 的主要原因通常不是技術上的唯一性需求,而是因為不想費心傳遞全域變數。如果打破 singleton 屬性不會造成嚴重問題,可以直接將 constructor 設為 protected 或 public,依靠團隊規範來確保只有一個實例。
The Case of the Horrible Include Dependencies#
C++ 的 include 機制是導致 build 緩慢的主要原因。一個 header 可以 include 另一個 header,層層累加後,一個小檔案可能間接 include 了數萬行程式碼。
解決策略#
- 盡量在 header 中使用前向宣告 (forward declaration) 而非 include
- 使用 Extract Interface 建立只包含純虛擬函數的 header
- 將 implementation 和 interface 分離到不同的 header 檔案
The Case of the Onion Parameter#
情境#
建立一個物件需要另一個物件,而那個物件又需要另一個物件… 像洋蔥一樣層層包裹。
解法#
- 使用 Extract Interface 在方便的層級切斷依賴鏈
- 使用 Pass Null 繞過不需要的參數
The Case of the Aliased Parameter#
情境#
一個參數的型別是來自第三方函式庫的 class,而該 class 又是許多其他 class 的基底 class。你無法輕易 extract interface 或 subclass。
解法#
- 如果語言支援,使用 Extract Interface 直接在第三方 class 上
- 或者建立一個 wrapper class 來封裝第三方 class,讓你能控制 interface
- 在 C++ 中,可以用 Subclass and Override Method 來避開問題
總結#
將 class 放入 test harness 的常見策略:
| 問題 | 常用技巧 |
|---|---|
| Irritating Parameter | Extract Interface, Pass Null |
| Hidden Dependency | Parameterize Constructor |
| Construction Blob | Extract and Override Factory Method, Supersede Instance Variable |
| Irritating Global Dependency | Introduce Static Setter, Subclass and Override Method, Extract Interface |
| Horrible Include Dependencies | Forward declaration, Extract Interface |
| Onion Parameter | Extract Interface, Pass Null |
| Aliased Parameter | Wrapper class, Subclass and Override Method |