這陣子有關 TDD 的辯論至此也大概差不多了,大師們還認真作了 幾個討論單元 。討論的好處是我們對於 TDD 的應用有更深入的了解,雖然結果仍是一樣:沒有銀彈,一切也要看情景而定。在這些討論中我們更清楚看到 TDD 的兩個面向:
1) 測試為安全網
TDD 帶來自動測試,設下了安全網,開發者可以在安全網內作出不同嘗試,而不失信心,也可以有更大的自由去重構,改善程式,適應需求,這使程式有能力演化,也是 Simple Design 的關鍵。可是測試就只是測試,單元 (unit) 或整合 (integrated) 測試都可以有安全網的效用。DHH 就認為其實 Integrated test 就夠了。這兩種測試當中有沒有分别?
有人會認為有:因為單元測試多是很專注的、隔離式的測試,通常一個單元測試壞掉,你很容易就能看出是那程式那一段那一句出錯。整合測試則很籠統,測試不通過時,還真不知道是那一個元件壞掉了。因此兩者還是有分別的。若果程式夠模組化,那有關模組之間如何組合應該會集中,如用 Spring 做 dependency injection 的話,有關 config 會落在一兩個 class 裏,那整合測試理論上只需證實這些 config 沒有錯就可以了。至於模組有否滿足所需功能,應用單元測試。這看法是以測試為安全網的角度看,有效測試理應直擊痛點,但也其實有一點點沾到有關設計面向,例如模組化。
另一個考慮是速度,若測試速度不夠快,例如要十多分鐘,開發者是沒有動力去常常測試的,結果你還是寫了一百行程式碼,然後才運行測試,看程式大爆炸。在這裏測試為安全網的成份會漸退,並漸漸拖開發者後腿。因此通常都會說,最好單元測試都在記憶裏運行,不碰 IO,一秒可執行幾百個。DHH 就認為所謂「單元測試才夠快」的想法已經過時,現在硬件夠快,SSD 效能夠,又可以用雲端 CI,整合測試也可以很快。然而,碰不碰 IO 除了是速度考量,也是一個設計考量,下一點會提到。
然而 TDDer 對速度的追求近乎即時,典型配置是在 IDE 儲存程式碼之際就自動執行測試,並將結果即時顯示。這並非只是為快而快,而是快速回饋讓開發者處於一種安穩的狀態,知道手下程式碼無誤,也有信心去探索寫法,毫不焦慮,提升效率。最近 WWDC 上發表新的語言 swift 所用的 IDE 就有一個近乎即時回饋的 playground,驚艷全場開發者,皆因短的回饋循環對開發實在有很大幫助。
2) 測試為設計工具
有些 TDDer 會告訴你,TDD 不是 Test Driven Developement,而應該是 Test Driven Design,設計是其目的,安全網只是一個 Good side-effect。一個很 難寫的測試正正是系統設計的臭味 ,可能一個物件有太 collaborators 或者要知道太多東西,以至其 setup 碼很長很難寫。因為不得不檢視物件間的關係,顯露了物件間的 coupling。
然而開發者首先得學懂聞臭味,才可以用 TDD 得出一個 low coupling 的設計,否則就很容易狂寫 Mocks 來代替 collaborators 來測試,結果是物件仍然臃腫,而且因為用 Mocks 測太多互動而 Refactor 不了,反而鎖死了設計。在 TDD 討論中,Kent Beck 就說他不太用 Mock。也有人提出例如 Mock 不過三層的原則 。
在這裏選擇那裏做邊界變得關鍵,例如 hexagonal structure 就說與邊界 IO (資料庫、瀏覽器、framework 等等) 設 adapters,adapters 與邊界可用 integrated tests,而邊界之內的就用 adapter 的 test double (mock/stub…) 與內裏元件做 unit test。這樣設計除了能好好隔離你核心的物件與周邊環境外,更可以有一堆好定義的 unit tests 邊界,tests 運行得夠快,只測外在行為也能夠方便 refactor。
DHH 對這種「隔離核心」的寫法很不以為然,認為那是過度設計。問人為何要隔離,通常就會舉例說如果將來要將程式 移植到另一個環境如 command line 之上,這種理由當然是很弱。Uncle Bob 寫過一篇有關 Framework 的文章,很露骨的比喻 Framework 應是秘書,問題是不應愛上秘書,給不應管的都給她管。這篇文因為後來有太多性別歧視而 改寫了 ,但不變的道理是:要知道界線。也可以反過來說,一個好的 Framework 應讓你清楚明白界線在那裏。
這種隔離技巧,或者可能算是多年經驗所得的結果?無論 design patterns 或 SOLID 其核心都是隔離。讀過一篇文就將 SOLID 都寫成 I don’t want to know… 原則 ,越是模組化的元件,就越不知道周圍環境的細節,只通過清晰接口溝通就好。常常說,我們建立 abstraction 就是為了打倒複雜度,然而 abstraction 本身的 indirection、新概念也會帶來理解和維護的複雜度,問題只是這整體的複雜度是否真的減小了,但這並不是簡單可計量的。
可是 TDD 其實只是曝露了 coupling,其他的設計特徵如 cohesion、encapsulation 等等都未可見。有人將依循多測試實踐的 Fitnesse 拿去做 dependency analysis ,得出來的 graph 還是很複雜。
除了有關元件設計外,TDD 中常用的招數是 triangulation,用測試 data 逐步「摸」出算法,也能幫助設計。不過其實重點還是 TDD 第三步 Refactoring 時開發者能否察看 duplication 再適時抽取 abstraction。
對於這兩個面向,安全網大概都是大家都很認同的,但說到測試的界線,如何影響到設計等,便有很多不同的意見。說句老話,工具都有意識形態偏向,關鍵在於我們有沒有知覺。
延伸閱讀: