當你有一個失敗的測試時,目標是盡快讓它變綠。本章的模式幫助你快速讓程式碼通過測試——即使結果不是你想長期保留的程式碼。
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 的手法,讓每一步只改動測試或只改動程式碼,從不同時改動兩者。這確保每一步都有安全網。