理想情況下,我們可以直接在測試框架中建立任何類別的物件、對它提問、驗證結果。但現實中,類別之間的相依性使這件事極其困難——建立一個物件可能需要連帶建立一整串其他物件,最終幾乎拉進整個系統。

打破相依性的兩個理由#

當我們想為程式碼加上測試時,需要打破相依性的理由可歸納為兩個:

  1. 感測(Sensing):當我們無法存取程式碼的計算結果時,打破相依性以便感測其內部行為
  2. 分離(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 就已足夠