文章

TDD 的兩個面向

這陣子有關 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。


對於這兩個面向,安全網大概都是大家都很認同的,但說到測試的界線,如何影響到設計等,便有很多不同的意見。說句老話,工具都有意識形態偏向,關鍵在於我們有沒有知覺。

延伸閱讀:

*