概述#
即使你成功地將 class 放入 test harness,你可能仍然無法執行特定的方法。常見的原因包括:
- 方法是 private,無法直接呼叫
- 在 test harness 中很難建構呼叫該方法所需的參數
- 方法有副作用(如修改資料庫、發送網路請求),我們需要在測試中感知或避開
- 我們需要透過方法使用的物件來感知 (sense) 方法的行為
The Case of the Hidden Method#
情境#
你需要修改一個 private method,但無法直接在測試中呼叫它。
策略#
首選方案:透過 public method 測試
如果能透過呼叫 public method 來間接測試 private method,這通常是最好的做法。private method 之所以存在,是因為它被某個 public method 使用。
但如果 private method 很複雜呢?
有時候 private method 包含複雜的邏輯,你真的想針對它單獨寫測試。這時有幾個選擇:
將 private method 改為 public – 這聽起來可怕,但如果你覺得需要獨立測試一個方法,這可能暗示該方法應該屬於另一個 class。如果是這樣,就提取一個新 class,讓該方法成為新 class 的 public method。
將 private 改為 protected – 然後在測試中建立子類別來存取它。這是一個折衷方案。
在 Java 中使用 package-private (default) 存取權限 – 將測試放在相同的 package 中。
如果你覺得需要測試一個 private method,這是一個設計信號:該方法可能承擔了太多責任,值得被提取到自己的 class 中。
提取到新 Class#
與其暴露 private method,更好的做法通常是:
- 識別 private method 的職責
- 建立一個新 class 來承載這個職責
- 讓新 class 的方法成為 public
- 在新 class 上寫測試
- 原始 class 持有新 class 的實例並委託呼叫
The Case of the “Helpful” Language Feature#
情境#
某些語言特性讓方法難以在 test harness 中執行:
- Sealed / Final class – 無法子類別化
- Final / Non-virtual method – 無法覆寫
- 語言中的建構限制 – 例如 C++ 的 constructor 中虛擬函數不會呼叫衍生類別版本
解法#
對付 Final/Sealed class:
如果你依賴一個 final class,你無法建立它的子類別來做 fake。解決方案:
- 使用 Extract Interface – 抽取 interface 後,你可以自由地建立 fake implementation
- 使用 Adapt Parameter – 包裹這個 final class,建立一個你可以控制的 wrapper
對付 Final/Non-virtual method:
- 在 Java 中,如果一個方法是 final 的,你無法在子類別中覆寫它。考慮使用 Extract Interface 或 Adapt Parameter。
- 在 C++ 中,non-virtual method 不會被 override。確保你想要覆寫的方法在基底類別中是 virtual 的。
語言特性的「保護」(如 final、sealed)在不同情境下的價值不同。在 production code 中,它們防止不當的繼承。但在 testing 情境中,它們可能成為阻礙。Extract Interface 幾乎總是能繞過這些限制。
The Case of the Undetectable Side Effect#
情境#
一個方法做了一些事情,但你無法在測試中偵測到它做了什麼。例如:
- 方法更新了 UI 但沒有回傳值
- 方法送出了網路請求
- 方法寫入了資料庫
- 方法修改了某個你無法存取的物件的狀態
範例#
假設有一個 AccountDetailFrame class(UI class),其中的方法直接操作 UI 元件並呼叫遠端服務:

Figure 10.1: AccountDetailFrame
public class AccountDetailFrame extends Frame {
private TextField display = new TextField(10);
...
public void performCommand(String command) {
try {
if (command.equals("deposit")) {
int amount = Integer.parseInt(display.getText());
// 直接呼叫遠端服務
account.deposit(amount);
display.setText(String.valueOf(account.getBalance()));
}
} catch (Exception e) {
display.setText("Error: " + e.getMessage());
}
}
}問題:
- 方法直接操作
TextField,這是 UI 元件 - 方法呼叫
account.deposit(),可能連接資料庫 - 無法在沒有 UI 的測試中感知結果
解法:Extract and Override#
步驟一:識別需要感知和分離的部分
- 感知 (Sensing):display 的文字內容
- 分離 (Separation):account 的行為
步驟二:Subclass and Override Method
建立測試用的子類別:
public class TestingAccountDetailFrame extends AccountDetailFrame {
private String displayText = "";
private int lastDepositAmount = 0;
@Override
protected void setDisplayText(String text) {
displayText = text;
}
@Override
protected String getDisplayText() {
return displayText;
}
public String getLastDisplayedText() {
return displayText;
}
}步驟三:將直接操作提取為可覆寫的方法
在原始 class 中:
public class AccountDetailFrame extends Frame {
...
protected void setDisplayText(String text) {
display.setText(text);
}
protected String getDisplayText() {
return display.getText();
}
public void performCommand(String command) {
try {
if (command.equals("deposit")) {
int amount = Integer.parseInt(getDisplayText());
account.deposit(amount);
setDisplayText(String.valueOf(account.getBalance()));
}
} catch (Exception e) {
setDisplayText("Error: " + e.getMessage());
}
}
}現在可以在測試中使用 TestingAccountDetailFrame,不需要真正的 UI 元件就能感知方法的效果。

Figure 10.2: AccountDetailFrame crudely refactored
其他 Sensing 技巧#
- Extract Interface 在依賴的物件上建立 interface,傳入可以記錄呼叫的 fake
- Break Out Method Object – 將整個方法抽取為一個新 class,讓你能更容易地替換依賴
- 使用 mock objects 來驗證方法是否以正確的參數呼叫了正確的依賴
關鍵原則是分離感知和分離 (sensing and separation)。Sensing 讓你驗證方法做了什麼;Separation 讓你隔離不想在測試中觸發的副作用。
常用技巧總結#
| 問題 | 常用技巧 |
|---|---|
| Private method 需要測試 | 提取到新 class、改為 protected + subclass、package-private |
| Final/Sealed class 阻礙 fake | Extract Interface, Adapt Parameter |
| Final method 無法覆寫 | Extract Interface, 委託到可替換的物件 |
| 方法有無法偵測的副作用 | Subclass and Override Method, Extract Interface + mock |
| 方法直接操作 UI 元件 | 提取 UI 操作為可覆寫方法, Subclass and Override |
| 方法呼叫外部服務 | Extract Interface 在服務上,傳入 fake |
核心觀念#
當你無法在 test harness 中執行一個方法時,問自己兩個問題:
- 我能感知到方法的效果嗎? 如果不能,需要引入 sensing 機制。
- 我能在不觸發副作用的情況下呼叫它嗎? 如果不能,需要 separation 技巧。
大多數情況下,Extract Interface 和 Subclass and Override Method 這兩個技巧就能解決問題。