當你有一個失敗的測試時,目標是盡快讓它變綠。本章的模式幫助你快速讓程式碼通過測試——即使結果不是你想長期保留的程式碼。

Fake It (‘Til You Make It)#

問題: 有了失敗的測試,第一步實作該怎麼寫?

解法: 回傳常數。測試通過後,再逐步把常數轉換成使用變數的表達式。

xUnit 實作中的例子:

return "1 run, 0 failed"

逐步變成:

return "%d run, 0 failed" % self.runCount

再變成:

return "%d run, %d failed" % (self.runCount, self.failureCount)

Fake It 就像攀岩時在頭頂打入岩釘——你還沒真正到達那裡(測試通過了但程式碼結構不對),但當你到達時你知道自己是安全的(測試依然會通過)。

Fake It 之所以強大,有兩個效果:

  • 心理層面: 綠燈和紅燈的感覺完全不同。綠燈時你知道自己站在哪裡,可以有信心地重構
  • 範圍控制: 從一個具體例子開始再泛化,避免過早被無關的顧慮搞混。你能更好地專注解決眼前的問題

重點: Fake It 是否違反「不寫不需要的程式碼」?不會——在重構步驟中,你要消除的是測試案例和程式碼之間的資料重複。例如:

// 測試
assertEquals(new MyDate("28.2.02"), new MyDate("1.3.02").yesterday());

// Fake Implementation — 與測試有資料重複
public MyDate yesterday() {
    return new MyDate("28.2.02");
}

// 消除重複後的真正實作
public MyDate yesterday() {
    return new MyDate(this.days() - 1);
}

Triangulate#

問題: 如何以最保守的方式驅動抽象?

解法: 只在有兩個或更多具體例子時才進行抽象。

public void testSum() {
    assertEquals(4, plus(3, 1));
}

private int plus(int augend, int addend) {
    return 4;  // Fake It
}

加入第二個例子後才抽象:

public void testSum() {
    assertEquals(4, plus(3, 1));
    assertEquals(7, plus(3, 4));
}

private int plus(int augend, int addend) {
    return augend + addend;  // 現在才泛化
}

補充: Triangulation 的規則看似清晰,但會製造邏輯迴圈——抽象完成後,其中一個斷言變得完全冗餘,可以刪掉。但如果刪掉,實作又可以退化為常數,又需要加回斷言。作者只在非常不確定正確抽象時才使用 Triangulation,其他時候偏好 Fake It 或 Obvious Implementation。

Obvious Implementation#

問題: 簡單的操作怎麼實作?

解法: 直接寫出來。

Fake It 和 Triangulation 是非常小的步伐。當你確信知道怎麼實作時,直接做就好。例如 plus() 這麼簡單的函式,通常不需要 Fake It。

但要注意——只用 Obvious Implementation 等於要求自己每次都完美。心理上這可能很有壓力:如果你寫的不是最簡單的改動呢?如果搭檔指出更簡單的做法呢?

注意: 留意你使用 Obvious Implementation 時被紅燈嚇到的頻率。如果頻繁發生(特別是 off-by-one 錯誤和正負號錯誤),就該降檔回 Fake It 或 Triangulation。

核心原則: 同時解決「讓它動」和「讓它乾淨」可能太多了。一旦發現太吃力,先退回只解決「讓它動」,之後再從容地處理「讓它乾淨」。

  • Obvious Implementation 是二檔
  • Fake It 是一檔
  • 當你的大腦開出你的手指無法兌現的支票時,準備好降檔

One to Many#

問題: 如何實作處理集合的操作?

解法: 先實作不含集合的版本,然後再讓它支援集合。

範例——寫一個加總陣列數字的函式,從單一值開始:

public void testSum() {
    assertEquals(5, sum(5));
}

private int sum(int value) {
    return value;
}

接著加入集合參數(Isolate Change——改變測試但不影響實作):

public void testSum() {
    assertEquals(5, sum(5, new int[] {5}));
}

private int sum(int value, int[] values) {
    return value;
}

改用集合來計算:

private int sum(int value, int[] values) {
    int sum = 0;
    for (int i = 0; i < values.length; i++)
        sum += values[i];
    return sum;
}

刪除不再需要的單一值參數(再次 Isolate Change——改變程式碼但不影響測試):

public void testSum() {
    assertEquals(5, sum(new int[] {5}));
}

private int sum(int[] values) {
    int sum = 0;
    for (int i = 0; i < values.length; i++)
        sum += values[i];
    return sum;
}

最後豐富測試:

public void testSum() {
    assertEquals(12, sum(new int[] {5, 7}));
}

技巧: One to Many 的關鍵是透過 Isolate Change 的手法,讓每一步只改動測試或只改動程式碼,從不同時改動兩者。這確保每一步都有安全網。