利用雙環 TDD 進行由外向內的開發

yongli92發表於2018-01-17

利用雙環 TDD 進行由外向內的開發

在我上一篇 文章 中,我開始討論倫敦派測試驅動開發(TDD),以及我認為它和傳統 TDD 不同的兩個特點。第一個是利用雙環 TDD 進行由外向內的開發,我將在這篇文章中詳細討論。第二點是「說,而不是問」的物件導向設計,我將在 下一篇文章 中再作討論。

雙環 TDD

london_school_001

當你進行雙環 TDD 時,你在內環上花費的時間是以分鐘計的,而在外環上花費的時間是以小時或天計的。外環測試是從系統的外部使用者的角度來寫的,通常覆蓋了粗粒度的功能,並且已部署在真實的(或至少接近真實的)環境中。在 我的書中 我把這類測試稱之為「指導測試」(Guiding Test),而 Freeman 和 Pryce 稱之為 「驗收測試」(Acceptance Tests)。這些測試應當在客戶期望不能滿足時失敗 —— 換而言之,它們提供了良好的回溯保護。它們也記載了系統應有的行為。(另見我的文章「敏捷自動化測試設計的原則」)

我不認為雙環 TDD 是倫敦派 TDD 特有的,我相信傳統 TDD 開發者也會採用。這一理念早在 Kent Beck 的第一本關於極限程式設計的書中就存在了。但我認為倫敦派的獨到之處在於由外向內的設計,並且輔之以 mock 的使用。

由外向內的設計

如果你使用雙環 TDD,通常你會先寫一個指導測試來體現一個使用者是如何與你的系統互動的。這個測試會幫助你確定位於最頂層,被首先呼叫的,作為需求功能的入口點的函式或類。這常常是一個 GUI 元件,一個網頁上的連結,或是一個命令列標誌。

而對倫敦派 TDD 而言,等你開始設計那些由該 GUI 元件、網頁連結或是命令列標誌來呼叫的內環 TDD 的類或方法的時候,你很快就會意識到這些新的程式碼無法由自己來實現整塊功能,而是需要其它的協作類來共同完成。

london_school_003

使用者觀察系統,並且期望某些功能。這意味著系統的邊界需要一個新的類。而這個類又進而需要更多尚未存在的協作類。

這些協作類尚不存在,或者至少不能提供你需要的全部功能。與其在此時暫停 TDD 而去立刻開發這些新的類,你其實可以在測試中將它們替換為 mock。在你將介面和協議開發到滿足需求之前,更換 mock 和實驗程式碼通常是很容易的。如此一來,當你在設計測試用例的同時,你也在設計生產程式碼了。

london_school_004

你可以將協作物件替換為 mock,這樣你就能設計它們之間的介面和協議了。

當你對你的設計滿意了,並且測試也通過了以後,你就可以深入下一層開始真正實現一個協作類。當然,如果某個類又進一步需要其它協作者,你可以再將它們替換為 mock 來進一步設計這些介面。這一方法可以持續整個系統設計,通達各個架構層和抽象層。

london_school_005

你已經完成了系統邊界的類,現在你可以開發它的某個協作類,並且用 mock 替換這個類進一步需要的協作類。

這一工作方式可以讓你把問題分解成可控的部件,在你每開始一個新部件之前都能把當前的部件詳細規定、充分測試。你能從關注使用者需求開始,然後由外向內地構建,在系統中一個部件一個部件地追蹤使用者互動的全過程,直到指導測試可以通過。通常在指導測試中不會將系統的部件替換成 mock,這樣最終當指導測試通過的時候你就可以確信你沒有忘記實現任何一個協作類。

在傳統 TDD 中由外向內

在傳統 TDD 的方法中也可以由外向內,但是用一種幾乎不需要 mock 的方法。存在幾種不同的策略來解決「協作類尚不存在」的問題。其中一種是從退化的用例開始設計,此時從使用者視角來看幾乎什麼都沒發生。這是一種當輸出比實際用例,或者愉快路徑要簡單得多的時候的特例。這樣你就能只用最基礎的空實現,或者假的返回值來構建這一簡化版的功能需求所需要的類的結構和方法。一旦結構有了,你就可以充實它(或許由內而外地進行也行)。

另外一種在傳統 TDD 中由外向內的策略是,先由外向內地寫測試,而當你發現你無法在某個協作類被實現之前使測試通過之時,就註釋掉那個測試,轉而去實現所需的協作類。最終你會發現你可以僅憑已經存在的協作者,就完全實現某個類,由此再逐步向上實現。

由外向內有時在傳統 TDD 方法中也許根本行不通。你會從系統中心的某個類開始,挑出某個僅憑已有的協作者就能完全實現和測試的部件。這通常是應用的領域模型的中心的一個類。當它完成以後,你再由中心向外繼續開發系統,一個一個地新增新的類。因為只使用已有的類,你就幾乎不需要使用 mock。最終你也會發現你完成了所有功能,也通過了指導測試。

優缺點

我認為由外向內的方法是有顯著優勢的。它能幫助你持續關注使用者的真正所需,使你構建一些真正有用的東西,而避免浪費時間粉飾打磨使用者不需要的。我認為無論對傳統 TDD 還是倫敦派 TDD 來說,由外向內的方法都需要技巧和訓練。學會如何將功能拆解成你能一步一步來開發和設計的增量部件並非易事。但是如果你由中心向外工作,就存在你會構建使用者不需要的東西的風險,或者當你抵達外層卻最終發現系統並不適用,而不得不進行重構。

然而,假設你已經是由外向內工作的了,我仍認為,取決於你是在真正的生產程式碼中編寫假的實現,還是隻在 mock 中寫,這兩者是有所不同的。如果你在生產程式碼中寫,你就逐步需要把它們取代為真正的功能。而如果你把假的功能放在 mock 中,它們就能永遠存在於測試程式碼中,即使當真的功能已經實現了,它們還在那兒。這對於程式文件很有用,也能讓你的測試得以繼續快速執行。

話雖如此,也存在一些關於在測試中使用了很多 mock 之後的可維護性的爭議。當設計更改時,除了生產程式碼外還要更新所有的 mock 也許代價太大了。一旦真正的實現完成了,或許內環測試就應該被刪除?畢竟指導測試已經能提供你需要的全部回溯保護了,因此那些僅僅對你最初的設計有用的測試並不值得保留?我不覺得這樣做就是毫無指摘的。從我和一些倫敦派支持者的討論來看,即使他們也會刪除部分的測試,但他們並不會刪除所有使用了 mock 的測試。

我也仍在嘗試理解這些爭端,並且試著找出在怎樣的場合裡倫敦派 TDD 可以帶來最大的收益。我希望我已經概述了由外向內的開發中,各種方法的區別。在我的下篇文章中,我將探討倫敦派 TDD 是如何推廣「說,而不是問」的物件導向設計 的。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章