概述#

即使你成功地將 class 放入 test harness,你可能仍然無法執行特定的方法。常見的原因包括:

  1. 方法是 private,無法直接呼叫
  2. 在 test harness 中很難建構呼叫該方法所需的參數
  3. 方法有副作用(如修改資料庫、發送網路請求),我們需要在測試中感知或避開
  4. 我們需要透過方法使用的物件來感知 (sense) 方法的行為

The Case of the Hidden Method#

情境#

你需要修改一個 private method,但無法直接在測試中呼叫它。

策略#

首選方案:透過 public method 測試

如果能透過呼叫 public method 來間接測試 private method,這通常是最好的做法。private method 之所以存在,是因為它被某個 public method 使用。

但如果 private method 很複雜呢?

有時候 private method 包含複雜的邏輯,你真的想針對它單獨寫測試。這時有幾個選擇:

  1. 將 private method 改為 public – 這聽起來可怕,但如果你覺得需要獨立測試一個方法,這可能暗示該方法應該屬於另一個 class。如果是這樣,就提取一個新 class,讓該方法成為新 class 的 public method。

  2. 將 private 改為 protected – 然後在測試中建立子類別來存取它。這是一個折衷方案。

  3. 在 Java 中使用 package-private (default) 存取權限 – 將測試放在相同的 package 中。

如果你覺得需要測試一個 private method,這是一個設計信號:該方法可能承擔了太多責任,值得被提取到自己的 class 中。

提取到新 Class#

與其暴露 private method,更好的做法通常是:

  1. 識別 private method 的職責
  2. 建立一個新 class 來承載這個職責
  3. 讓新 class 的方法成為 public
  4. 在新 class 上寫測試
  5. 原始 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 InterfaceAdapt 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 阻礙 fakeExtract Interface, Adapt Parameter
Final method 無法覆寫Extract Interface, 委託到可替換的物件
方法有無法偵測的副作用Subclass and Override Method, Extract Interface + mock
方法直接操作 UI 元件提取 UI 操作為可覆寫方法, Subclass and Override
方法呼叫外部服務Extract Interface 在服務上,傳入 fake

核心觀念#

當你無法在 test harness 中執行一個方法時,問自己兩個問題:

  1. 我能感知到方法的效果嗎? 如果不能,需要引入 sensing 機制。
  2. 我能在不觸發副作用的情況下呼叫它嗎? 如果不能,需要 separation 技巧。

大多數情況下,Extract InterfaceSubclass and Override Method 這兩個技巧就能解決問題。