理想情況下,我們可以直接在測試框架中建立任何類別的物件、對它提問、驗證結果。但現實中,類別之間的相依性使這件事極其困難——建立一個物件可能需要連帶建立一整串其他物件,最終幾乎拉進整個系統。
打破相依性的兩個理由#
當我們想為程式碼加上測試時,需要打破相依性的理由可歸納為兩個:
- 感測(Sensing):當我們無法存取程式碼的計算結果時,打破相依性以便感測其內部行為
- 分離(Separation):當我們根本無法將一段程式碼放進測試框架中執行時,打破相依性以便分離它
偽造協作者(Faking Collaborators)#
Legacy code 中最大的問題之一是相依性。當我們想要單獨執行一段程式碼並觀察其行為時,往往需要打破對其他程式碼的依賴。解決方法是用偽造物件(Fake Objects) 取代真實的協作者。
Fake Objects#
Fake Object 是一個在測試中冒充真實協作者的物件。以收銀系統為例:
Sale類別有一個scan()方法,掃描條碼後需要在收銀機螢幕上顯示商品名稱與價格- 直接測試螢幕顯示很困難,因為螢幕 API 深埋在
Sale類別中

Figure 3.1: Sale
解決方案:引入介面(Extract Interface)
public interface Display {
void showLine(String line);
}
Figure 3.2: Sale communicating with a display class
讓 Sale 透過建構子接受任何實作 Display 介面的物件:
public class Sale {
private Display display;
public Sale(Display display) {
this.display = display;
}
public void scan(String barcode) {
// ...
String itemLine = item.name()
+ " " + item.price().asDisplayText();
display.showLine(itemLine);
// ...
}
}測試時使用 FakeDisplay:
public class FakeDisplay implements Display {
private String lastLine = "";
public void showLine(String line) {
lastLine = line;
}
public String getLastLine() {
return lastLine;
}
}public class SaleTest extends TestCase {
public void testDisplayAnItem() {
FakeDisplay display = new FakeDisplay();
Sale sale = new Sale(display);
sale.scan("1");
assertEquals("Milk $3.99", display.getLastLine());
}
}Fake Objects 支援真實的測試。 有人可能質疑「這不是真的在測試螢幕顯示」。但測試的本質是分而治之——這個測試告訴我們
Sale物件會把什麼文字送到 Display,如果出了 Bug,它能幫助我們排除Sale的問題,大幅節省除錯時間。

Figure 3.3: Sale with the display hierarchy
Fake Object 的兩面性#
Fake Object 有兩個「面」:
- 面向被測物件的一面:實作與真實物件相同的介面(如
showLine方法),這是Sale物件看到的一面 - 面向測試的一面:提供額外的方法讓測試檢查結果(如
getLastLine方法),這是測試需要的一面
因此在測試中,變數型別應宣告為具體的 FakeDisplay 而非介面 Display,以便呼叫測試專用的方法。

Figure 3.4: Two sides to a fake object
Fakes 的精髓#
Fake Object 可以用多種方式實作:
- 在 OO 語言中,通常是實作相同介面的簡單類別
- 在非 OO 語言中,可以定義替代函式,將結果記錄到全域資料結構中供測試存取
Mock Objects#
當你需要撰寫大量 Fake Object 時,可以考慮使用更進階的 Mock Object。Mock 是內建斷言邏輯的 Fake:
public class SaleTest extends TestCase {
public void testDisplayAnItem() {
MockDisplay display = new MockDisplay();
display.setExpectation("showLine", "Milk $3.99");
Sale sale = new Sale(display);
sale.scan("1");
display.verify();
}
}Mock 的優勢是可以預先設定期望,然後統一驗證。但 Mock 框架並非所有語言都有支援,在大多數情況下,簡單的 Fake Object 就已足夠。