章節概述#

本章處理 Dollar 物件的 side effect 問題——呼叫 times() 會改變原始物件的值。作者透過改變介面讓 times() 回傳新物件來解決問題,並藉此介紹 TDD 中快速讓測試變綠的三種策略。

TDD 的一般循環#

作者在本章開頭完整闡述了 TDD 的三階段循環:

  1. 寫一個測試(Write a test):想像你希望操作在程式碼中呈現的樣貌,你在寫一個故事。設計你理想中的介面,包含計算正確答案所需的所有元素。

  2. 讓它通過(Make it run):快速讓進度條變綠,這是最高優先。如果乾淨簡單的解法顯而易見就直接寫;如果需要花一點時間,先記下來,先用最快方式讓測試通過。快速綠燈可以暫時赦免一切罪過,但只有一瞬間。

  3. 讓它正確(Make it right):系統行為正確後,回頭消除剛才引入的重複,走回軟體正道。

重點: 目標是「能運作的乾淨程式碼」(clean code that works)。策略是分而治之——先解決「能運作」,再解決「乾淨」。這與架構驅動開發(先求乾淨再設法讓它動)的方向恰好相反。

發現 Side Effect 問題#

上一章的 Dollar 有一個問題:呼叫 times() 會修改原始物件。如果我們嘗試對同一個 five 連續呼叫兩次乘法:

public void testMultiplication() {
    Dollar five = new Dollar(5);
    five.times(2);
    assertEquals(10, five.amount);
    five.times(3);
    assertEquals(15, five.amount);
}

第二個 assertion 會失敗,因為執行完 five.times(2) 後,five 的值已經變成 10 了,再乘以 3 會得到 30,而非預期的 15。

解法:回傳新物件#

找不到讓現有測試乾淨通過的方法,所以作者改變了介面——讓 times() 回傳一個新的 Dollar 物件,而非修改原始物件。測試隨之更新:

public void testMultiplication() {
    Dollar five = new Dollar(5);
    Dollar product = five.times(2);
    assertEquals(10, product.amount);
    product = five.times(3);
    assertEquals(15, product.amount);
}

補充: 改變介面沒關係。對介面的猜測不會比對實作的猜測更完美——發現更好的設計時就該調整。

先讓測試能編譯——times() 改為回傳 Dollar,暫時 return null:

Dollar times(int multiplier) {
    amount *= multiplier;
    return null;
}

測試能編譯了但會失敗——這就是進步。接著直接寫出正確的實作:

Dollar times(int multiplier) {
    return new Dollar(amount * multiplier);
}

測試通過,綠燈亮起。

三種快速讓測試變綠的策略#

本章介紹了三種策略中的兩種(第三種在 Chapter 3 示範):

  • Fake It(假裝):先回傳一個常數,然後逐步用變數取代常數,直到得出真正的程式碼。Chapter 1 就是用這個策略。
  • Obvious Implementation(顯而易見的實作):當你清楚知道正確的實作是什麼,就直接寫上去。本章用的就是這個策略。

技巧: 在實務中,這兩種模式會交替使用。一切順利時,連續使用 Obvious Implementation(每次都跑測試確認)。一旦遇到意外的紅燈,就退回 Fake It 模式,透過重構找到正確的程式碼。信心恢復後,再切回 Obvious Implementation。

flowchart TD
    A["寫一個失敗的測試"] --> B{"有信心直接實作?"}
    B -->|"是"| C["Obvious Implementation<br/>直接寫出正確實作"]
    B -->|"否"| D["Fake It<br/>先回傳常數"]
    C --> E{"測試通過?"}
    D --> E
    E -->|"🟢 通過"| F["重構消除重複"]
    E -->|"🔴 失敗"| G["退回 Fake It"]
    G --> D
    F --> A

將感覺轉化為測試#

重點:直覺感受(例如對 side effect 的不舒服感)轉化為具體的測試案例(例如對同一個 Dollar 乘兩次),是 TDD 中反覆出現的主題。隨著練習越多,你越能將美學判斷轉化為測試。當你能做到這一點,設計討論就會變得更有趣——先討論系統應該如何運作,決定正確行為後,再討論實現的最佳方式。

本章小結#

本章的要點回顧:

  • 將一個設計上的不滿(side effect)轉化為一個會因此失敗的測試案例
  • 用 stub 實作快速讓測試編譯通過
  • 直接寫入看似正確的實作來讓測試通過
  • 介紹了 Fake ItObvious Implementation 兩種策略