分類
book

《TDD By Example》範例說明

學習 TDD 必看的聖經本,當然就是 Kent Beck 的 TDD by Example。在取得 Kent Beck 授權之後,將書中每一步程式碼的演進,都放到 GitHub 的 commit history 上分享給各位讀者參考,讓大家既能專注在每一步的變化,又能兼顧全局,還可以在任何一步的 sample code 繼續往下練習。

如果你需要能幫助你以更實際、更有敢的例子,更系統化學習能直接在實務上的 TDD 技能樹,請參考這兩門不同切入點的培訓內容:

  1. Classic TDD by Example: 這是一門無限次、不限時、可重複觀看(事實上培訓內容中的題目,你至少要重練 5~6 次以上,才能對每一個設計決策的分支點做嘗試與學習)的影音培訓內容,重點放在從需求釐清開始,一直到實際 TDD (真正實作的部份,核心都在重構,設計是重構來的)來完成最簡潔、好懂、好擴充的產品設計
  2. TDD與持續重構:這門課則是用2天的時間來模擬演練,當團隊碰到一個真實的需求時,會面臨到哪些問題、該怎麼進行,為什麼會寫出一堆不可測試、糟糕設計、充滿 code smell 的程式碼,而該怎麼基於前人這堆技術債的 legacy code 來重構(我們會一起 code review 各小組在現場完成的 test code 與 production code,有哪些 code smell,並從中挑選髒的最具代表性的程式碼,來 live 重構給各位看,面對再髒的 legacy code,該怎麼著手重構,該怎麼用 TDD 來解決我們碰到的問題)

GitHub Commit History

購買書籍

勘誤

  • 第六章,圖 6-1,應為下圖
  • 第11章,sample code 11-1,原文標示針對 Franc 與 Dollar 改回傳 Money,但其實這兩段 code 都是在 Money class 裡面,目的是用來移除對 Franc 與 Dollar 的參考。(請參考 GitHub commit
在 Money class 中移除對 Franc 與 Dollar 的參考
  • 第 14 章,p.79 漏了一段測試程式碼。
少了一段測試程式碼
public void testIdentityRate() {
    assertEquals(1, new Bank().rate("USD", "USD"));
}

譯者序

在 2017 年時翻譯了《單元測試的藝術》之後,以及幾年來場場爆滿的《針對遺留代碼加入單元測試的藝術》培訓,收到了許多讀者與培訓學員的熱烈反應,那本書讓他們開始在實際產品開發過程中進行單元測試,在原本遺留產品茫茫大海中逐漸形成一塊立足之地,能以此自動化測試作為保護網,逐漸在這基礎上拓展綠地,安全穩定地在上面構築與有自信地交付新的產品功能。

現在,有單元測試的基礎之後,就該繼續邁開下一步了。有了測試保護,就可以開始安全地進行重構,有重構,才會有設計;有重構,設計才有品質。該如何在需求、文件、測試、開發、設計(廣義的軟體開發)各環節以一貫之,用最有效益的方式進行軟體開發呢?測試驅動開發(Test-Driven Development, TDD)!一種以測試為開發輔助、以測試來描述需求情境、以測試來當作目標、以測試來表達期望、以測試來驗證疑問、以測試來實驗學習、以測試來溝通協作、以測試來協助設計高易用性 API 的「開發方法」。

軟體開發是一種知識工作型態,而它與其他工作或行業相比,最特別的一點莫過於「不確定性」。而 TDD 與敏捷,正是應對不確定性與變化的通用解決方案。當在高速公路上目的地明確,晴空萬里、一路通暢,我們可以踩下油門,有自信地大步加速前進。當在雪地上開車,輪胎在路上容易打滑,或是大霧迷濛不確定路況時,我們透過測試來增加摩擦地、抓地力,以穩定狀態。或是在陌生的城市中開著車,測試如同車上導航一般,讓我們設定目的地,在我們走錯路時第一時間提醒我們。讓事情能持續地避免失控,讓我們能每走一步就能確認自己的方向、現在路況仍安全且正確。讓我們在最一開始「犯錯」時,就能透過測試執行結果的反饋,來幫助我們即時拉杆回來,避免走了太多冤枉路。既然犯錯是避免不了的,我們能做的只有讓錯誤早點被發現、讓錯誤的成本夠低足以承受、讓錯誤還在很小的時候就被解決。

我們所面對的企業級應用系統需求往往十分複雜,想像我們要完成每一個大小不一的功能,就像在水井裡面打水一樣,如果這個功能越大,就代表水桶越大,而每個人的狀態跟力氣有限,稍有不慎就可能前功盡棄。而 TDD 在一開始就評估這次要打上來的份量有多大,我們是否能穩定、快速、正確地把水打上來。如果沒有信心,我們就透過拆分需求來縮減水桶大小,以因應我們現在狀態調整到最佳的處理方式。而透過自動化測試的建立,就像在打水的旋轉把手加上了卡榫的設計,當每往上拉上來一點,就能被卡榫保護,休息一下,調整一下狀態,接續前進,讓我們在心理跟生理能持續調整保持最佳狀態。如果你的力氣不夠、突然狀態不好,透過極限程式設計中結對的副駕駛來接手,過程中執行命令的複誦與確認,能大幅降低錯誤發生的機率,以及對每一位駕駛的身心負擔。TDD 就是這樣一種讓你能化繁為簡,自由控制開發步伐大小,以因應不確定性(如需求、技術、環境)、限制(如時間、能力、資源)、狀態(如信心、壓力、健康、心情),持續調整出最適當的步伐大小。

TDD 透過紅燈、綠燈、重構的循環,形成一種開發節奏。在開發的過程中,不再是一頭栽入程式碼的世界,不再是邊寫邊想,而是以終為始,先用測試畫靶,接著才射箭。在開始撰寫產品程式碼之前,Think First。想好我們等等想完成的功能內容,是如何被使用者或使用端使用,然後用測試來描述這樣的使用情境,就像電器的使用說明書透過範例來介紹功能一樣。你該先準備什麼、然後做什麼,接著預期結果會怎樣。當你有所疑慮,不知道某個作法的結果、某個第三方套件執行結果、甚至是某個尚未嘗試過的 API,執行起來是不是會如你心裡所想一樣,把你的疑慮轉換成用測試來呈現,這樣的實驗與學習,除了能讓技能經驗累積成長以外,還能免除你心裡的疑慮恐懼。當心無旁鶩時,自然能發揮出最佳戰力。

而這些測試程式在經過重構之後,就能像文件一般,留給未來維護的人一份產品的使用指南,他們(絕大部分情況就是未來的你自己)不再只能透過 trace 那龐大複雜且充滿臭味的產品程式碼流程,不再需要猜測為什麼要有這功能、為什麼要這樣實作、這功能到底該怎麼用、這些參數到底要傳什麼才是對的,我到底該相信註解、還是我理解的程式碼呢?都不是,相信測試,相信測試的結果,因為程式是照寫的跑,不是照想的跑。

當用測試將靶畫好、目標定好,雖然我們還沒完成它,但我們知道我們已經準備好了。就像攀岩時我們會先在頭頂上方適合的位置打上岩釘,雖然我們還沒移動到那上面,但你知道當你移動到那邊時會是安全穩固的。接下來只要安全、迅速地移動到新的岩釘上,我們距離攻頂就更近一步了。在過程中我們希望能專注、迅速、集中力量達成目標,其他可能衍生出來的需求或任務,我們只須加入待辦清單中,等待後續循環再用測試來啟動它們。這種單一專注性,就像攀岩時四肢同時只會移動一隻,它能帶給你最高效率、最安全的攀登狀態。

而在每次 TDD 循環中,最後都會透過重構來讓產品程式碼在正確性上更貼近商業邏輯需求,在設計上能達到剛好才是最好的簡單設計,因為預測未來的設計總是失準的。也因為每個循環都會進行重構,因此清理技術債本身就是開發循環中的一部分。就像壽司師父處理完一道料理之後,都會去清理刀子、料理台一樣,這樣每一次要處理並端出新料理,才不會有壞味道、副作用,才能安全穩定地產出高品質的產品供客戶享用。

讓我再強調一次,需求、文件、測試、開發、重構、設計,本來就都是廣義的軟體開發該進行的工作,甚至是同一份工作,而 TDD 就是能用同一份且已知最小的付出,同時獲得這些面向最大效益的一種開發方式。

本書如果要摘要3個口訣,我會選擇的是:

  • 紅燈、綠燈、重構
  • Fake it till you make it
  • Make it work, make it right, make it fast

強烈建議大家一定要熟練「假實作」跟「三角定位法」兩種紅燈變綠燈的作法。透過範例,本書前兩部份的重點在於對需求的拆解、剛好的設計、有機並且以演進方式的開發過程來闡述 TDD 的開發方式。在此感謝 Odd-e 同事 Stanly 的協助,讓我得以與本書作者Kent Beck 聯繫上,並取得他的允許(非常感謝 Kent 的慷慨大方),將書中前兩部份的範例修改歷程,放到 GitHub 上公開呈現每一個步驟的演進,作者與我都相信這樣能幫助所有讀者更清楚看到全貌、更專注了解每一步的變化,相關範例與說明請見:https://book.tdd.best/

總結一下 TDD 的好處:

  • 幫助你化繁為簡,步伐因應狀態可大可小
  • 可幫助你實驗、學習、獲得反饋來輔助設計決策
  • 讓你去除心中疑慮、讓你明確了解產品執行狀態是否符合預期、讓你專注、讓你得以減壓、喘息、調整狀態,讓系統與團隊都得以持續在穩定的狀態下接續開發或調整設計
  • 讓團隊能不受時空限制,透過可執行的文件,得以有效溝通

最後我想以我個人經驗補充一下,TDD 本身是一門綜合戰技,在實務上如果要熟練地發揮 TDD 的綜效,除了書中的內容以外,還須加強與具備的技能如下:

  1. 自動化測試相關:包含了測試框架、Mock 技術、驗證套件,及重構測試。除了單元測試,建議還要熟悉顆粒度更大的驗收測試,以及將需求拆解成情境的實例化需求技術。
  2. 重構相關:包含 code smell、重構技巧,以及重構與設計相關的模式。
  3. 開發速度:因為 TDD 會附帶著大量的測試程式碼,大量的重構動作,以及因為產品程式碼還不存在所產生的錯誤,如果開發速度慢,會因為量體變大而導致需要的時間更長,可能很多人就會卡在這一道門檻跨不過去。如果開發方式經過正確的鍛鍊,搭配 IDE 與 TDD 調整開發的方式與順序,開發速度將能獲得戲劇性的綜效。
  4. 極限程式設計與敏捷開發:持續整合、單元測試、重構、TDD、結對程式設計、code review、持續交付、實例化需求等等,甚至最前面的影響地圖、使用者故事地圖,掌握這些技能將使你以終為始、端到端交付的品質與速度更能維持穩定。

相關活動