為什麼要透過 The Three Laws of TDD 來限制 TDD 的過程,這近乎於不合理的三條限制法則,能帶來什麼好處?
The Three Laws of TDD 說明
Over the years I have come to describe Test Driven Development in terms of three simple rules. They are:
1. You are not allowed to write any production code unless it is to make a failing unit test pass.
2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
-The Three Laws of TDD, Uncle Bob
1. You are not allowed to write any production code unless it is to make a failing unit test pass.
在動手寫或修改任何一行 production code 之前,都應該要有目的,除非是為了重構。而這個目的、需求,甚至是疑惑,都應該要能用測試案例來描述出來。當透過測試案例描述完這個「待完成」的需求,自然就會得到一個紅燈,而實際動手異動任何一行程式碼,都是把完成這個需求當作目標。
如果先寫 production code 再加入測試案例,往往會在你寫 production code 時,目標與注意力容易發散而無法聚焦,你不知道什麼時候該停手,不知道自己是否還在前往目標的路上。
這也是我們常提到「先射箭,再畫靶」的盲點。你是先矇著眼踩著油門往前衝,等你感覺衝到目的地了,再把眼罩拿開,確認一下自己是不是到達目的地。
而有很多時候,你會因為已經花太多時間跟資源在踩油門的衝刺,所以就乾脆把目的地修改成你現在在的地方。所以,你的測試程式就無法表達出需求,而只是單純沒意義地驗證著 production code 是否執行無誤(do thing right),而非驗證是否正確(do right thing)。
2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
當你已經有一個失敗的測試案例時,要做的應該是修改 production code 以通過這個測試案例,而不是增加更多失敗的測試案例。當然,編譯錯誤也算錯誤。
正常情況下,在 TDD 的過程中,測試總管最多只會存在一個失敗的測試案例。而且這個失敗的測試案例,一定就是剛剛加入的 scenario ,同時也就是代表著目前 production code 要加進去支援的新功能。
只要同時存在著一個以上失敗的測試案例,就代表有異常狀況,代表這次修改的動作相互影響到其他測試案例。
在一開始學會使用測試案例來描述需求規格時,很容易就會一次把所有需求都描述完,在得到 N 個紅燈後,才開始動手開發 production code ,而且會以為自己在 TDD 的紅燈、綠燈、重構循環中。但事實上這樣反而會失去 TDD 最大的好處之一:聚焦。
因為一次想通過所有紅燈或是多個紅燈,就會讓需求顯得複雜,就枉費把需求拆分成多個 scenario 了。
一次只新增一個 scenario ,因為 production code “尚未支援” 這個新的 scenario ,所以測試結果為紅燈。透過紅燈的標示,可以很清楚地定義出來, production code 的目標就是要通過這個紅燈,讓它變成綠燈,也就代表 production code 在這個新的 scenario 底下可以如同預期般地運作。而且 production code 之前支援的所有 scenarios 也都如預期正常運作。
3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
你寫的每一行 production code 其目的應該都只為了讓眼前這個紅燈變成綠燈,也就是通過眼前這個失敗的測試案例。跟這個測試案例無關的程式碼,一行也不准多寫。
一次只新增一個失敗的測試案例,目的就是為了讓開發人員在調整 production code 時,可以完全聚焦在眼前這個新的 scenario 。過去已經支援的 scenario 都不用再考慮,只需要找到新的 scenario 應該從目前 production code 的哪一個商業邏輯岔出新的分支。
怎麼調整 production code 來通過眼前要新增的這個 scenario 呢?Baby step!
這是開發人員第二個常見的關卡,明明我就知道後面的需求,在這邊如果怎麼寫,我就可以一次滿足好幾個需求,為什麼我要傻傻地用 baby step 來寫看起來很愚蠢的 production code 呢? 因為用最少、最簡單、最直覺的方式來通過測試案例滿足需求,你新增程式碼的程度行數最少,或是最直覺,別忘了,我們這次只 focus 在這一次異動的程式碼。用越少、越簡單、越直覺地方式通過眼前這個測試案例,等等的重構動作就越輕鬆。因為你這次異動的程式碼相當少,重構當然就顯得輕鬆。
也因為每次只 focus 眼前這個 scenario 與之前所有 scenarios 不同的地方,並且用最簡單的方式來滿足這個新的 scenario,所以就可以有效地避免 over design 的情況發生。先有 code,再重構,可以有效滿足三個目的:
- 避免過度設計
- 即時交付
- 順手整理 production code 與清理 tech debt
也因為你的每一行 production code 都滿足著某一個 scenario,因為你的每一行 production code 都有著代表著 scenario 是否正常運作的測試案例保護,所以重構起來毫無風險。也因為你這次異動 production code 程式碼的幅度很小,所以重構的速度快、難度低、範圍小。
【注意】每一個 production code 的邏輯分支,其實都是被某一個獨特有代表性的 scenario 切出來的,也就是代表 key business logic 的 scenario 。
結論
TDD 除了最基本的「紅燈、綠燈、重構」循環外,在「紅燈變綠燈」的過程,建議遵循著 Uncle Bob 這三條簡單的規則:
- 透過測試案例來描述這次要調整 production code 的目的,在還沒有產生一個紅燈之前,不准動手改 production code 。
- 隨時應該只存在「最多一個」紅燈,當有紅燈的狀態時,應該動手調整 production code 以通過紅燈。
- 當在調整 production code 時,每一行程式碼應該都只針對眼前這個紅燈變成綠燈而寫,與這目標無關的,一行都不准寫。
當這樣子透過 baby step 從紅燈變成綠燈之後,很自然地就要對 production code 進行重構,因為剛剛 baby step 產生的 production code 容易產生一些 bad smells ,但這時重構的風險、成本跟範圍很低,因為 baby step 用最少、最直覺、最簡單的程式碼來滿足需求,程式碼異動的數量跟範圍越小,自然重構起來就越輕鬆。
所有已經 pass 的測試案例與重構完成的 production code ,在下一次新增需求時,都不需要重新再 trace 或瞭解一次,而只需要找到這一次新增的 scenario 該從 context flow 的哪一個點,切出新的分支,來代表這一次的 key business logic 。
在學會 TDD 之前,你的開發是像下圖一樣:
而在你使用 TDD 開發時,你的開發會變成這樣:
而且以上圖使用 TDD 的開發模式來說,每一條已經建立完畢的路徑,都不需要再重新 trace 或重寫一次,而只需直接找到該切出分支的點,開始走出新的 scenario 不一樣的商業邏輯。