Skip to main content

學習TDD的心路歷程

Opass
A life well lived

我們聽了太多的敏捷開發,但不寫測試的敏捷,就像不會動手術的外科醫生一樣荒謬。

我在2017年上了Daniel的CSM(Certificate Scrum Master)課,上完有種毀三觀的震撼,那門課表面上在講SCRUM,其實講的是一套敏捷的人生觀。課程最後一天的下午,Daniel談到了為什麼要自動化測試。

Daniel畫了一張圖,給了一個簡單的理由:雖著時間進展,軟體的功能會線性成長,但每次Release都需要測試全部的功能。不自動化測試的話,遲早有一天你的QA會爆掉。

當時第一次接觸到Scrum,意識到這是很有價值的方法論,回去公司後迫不及待分享給同仁。但那時的我是個無知的敏捷信徒,只會嚷嚷測試很重要,但卻不會寫測試。主管聽完後,思考了一下,表示實務上有困難,PM的需求朝令夕改,如果我替軟體加上測試,軟體會變得穩定,但之後要改架構會變得困難。故決定先暫緩這樣的想法,只在幾個特定的feature上加上自動化測試,未來需求比較穩定後再來試著導入看看。

後來因為其他緣故離開了那份工作,但直到現在,我依然不認為主管當時的決定是錯的。以當時Team的狀況要導入測試的確不容易。我沒有經驗,沒辦法 show, don't tell 直接拉起袖子做,是最主要的敗因。

下一份工作我遇到軟體品質的問題,團隊對於debug相當頭痛。我開始意識到測試的重要性,於是在9月和11月,主動去上了91的極速開發TDD重構課

為了確保花的學費有價值,上課前的一個月,我決定先在工作上練習寫單元測試。我還買了Modern C++ Programming with Test-Driven Development閱讀,這是本好書,裡面用Example示範TDD的步驟,還講了許多C++的Stub/Mock技巧。但讀完後,我還是不知道怎麼在範例外的問題上使用TDD,我不知道怎麼設計Test Case、不知道怎麼選擇下一個Test Case,書上講得頭頭是道,也有很多範例程式,但是距離實踐有一大段鴻溝。

上完這兩門課後覺得非常值得,最棒的是親眼看見講師示範「好的TDD開發實踐應該是什麼樣子」。沒有榜樣自己練TDD很容易練成九陰白骨爪。親眼看見會產生疑問,此時詢問講師並獲得反饋,等於直接吸納人家的經驗值,少走很多彎路。

極速開發教的是工具,解決沒有時間寫測試的問題。而TDD重構課大概是91系列課中最有價值的一門課,實際帶你演練一次高手的TDD和重構會是什麼樣子,課程講了實例化需求、測試案例排序。其中最震撼的是,講師讓我們分組寫一段程式,並讓我們改到覺得滿意為止。隔天現場示範重構,每個步驟都拆的很細。那時才意識到,原來我以前根本不會重構,我過去一直以為的重構,就只是抽方法和取一個好名字而已。而真正的重構是有測試作為防護網、有能力辨識程式壞味、有辦法小步地穩健修改。

上課容易,內化困難

這兩門課開了眼界,脫離了「不知道自己不知道什麼」的狀態。但我其實隱約察覺,自己還有很多疑惑,但經驗還不足,說不清楚問題是什麼。

我花了兩個禮拜用Visual Studio + Resharper練習C#的Tennis Kata,解決速度的問題 。下個問題立刻接著而來,我工作用的語言是C++,但上課用C# + Java + PHP,有點尷尬。我裝了Resharper C++,但因為語言特性的關係,並不像C#版的Resharper那麼好用。理想上,重構當然不需要工具,靠Compiler應該就能實現穩健的重構,Martin Fowler的「重構,改善既有的程式設計」就有描述這些手法,但有工具會讓整件事輕鬆很多。

另外C++ Legacy Code編譯時間很長,想要替既有的程式加上單元測試不切實際。必須先Decoupling,用一些降低相依性的技巧,將修改、編譯、連結的時間降低。但這又陷入雞生蛋蛋生雞的問題,需要測試才能保證重構不出事,但髒Code不經過重構很難加測試。Code Base一旦大到一個程度,所有事情都開始變得很棘手。

我試著挑了一個獨立的套件,用C++的Google Test做TDD,替程式加入簡單的新功能。還找同事一起Pair Programming了一個下午,順便帶一些TDD的觀念。同事覺得很穩健,但是開發速度太慢,其中的Dependency Injection技巧他也不夠熟悉,要應用在團隊中太難。

儘管已花很多時間學習,但在C++開發上還遠遠不夠。一方面語言本身坑很多。另外我還需要學Decoupling技巧降低編譯時間,這需要閱讀大型C++軟體開發。如果用火候來比喻我當時的TDD技能,大概還跟火柴一樣,除了上課的範例外,給一個稍微複雜的題目就會無從下手。而網路上針對C++的TDD範例並不多,相對其他語言學習更為坎坷。

我花了一點時間學習C++的重構技巧,閱讀Working Effectively With Legacy Code。後來因為其他因素離職,時間充裕了起來。我決定把C++先放一邊,改用我還不太熟悉、但資源較為豐富的C#練習TDD。

因為平常習慣使用Linux的關係,決定要學C#實在有點糾結。C#最主要的應用場景是Windows程式和ASP.NET。在Linux寫C#就像是去日本買大同電鍋一樣。不是做不到,但是這麼做的人還不太多。我裝了跨平台的C# IDE Rider,接著花了一個禮拜練習Tennis Kata並調整開發環境。一開始頗為忐忑,因為沒有範例影片可以模仿,有點擔心會出現一些難以處理的問題,但是觀念是通的,處理掉快捷鍵相衝,刻意練習一陣子後就成功換到Rider上了。

由於Tennis Kata已經練到爛了,我在網路上找了幾個不同的Kata題目練習。先從最簡單的羅馬數字轉換器開始,知道需求後,試著自己想Test Case、排序,接著試著寫。完全不意外,思考流程非常卡。我覺得我想的演算法不夠漂亮,但又還沒想到什麼更好的作法。硬著頭皮完成後,我參考youtube其他網友的練習,試著重構,並發現很多疑問,TDD有一些優點,但也有一些TDD幫不太上忙的地方。

後來我參加了91辦的第一屆Coding Dojo(代碼道場),這是個大家犧牲能夠睡到自然醒的假日,歡樂練習TDD的社群活動。30個人輪流上台接著用TDD寫Poker Hand Kata,非常有趣。在台下抱怨人家的Code怎麼寫的那麼爛,結果上台腦袋自動一片空白。活動結束後,我和紘銘抽了兩天用TeamViewer做Online Pair Programming by TDD,練習同一道題目。合計花了約略20個小時。練習過程中,想通了很多觀念,也連帶冒出許多更進階的問題。

  • Test Case是不可能精確、完整的描述需求的。舉例而言,Tennis Kata的比分可以有無限多種,但我們不可能列出所有的比分作為Test Case。
  • 理想上,每多加一個case,只要多寫一個判斷就行。因為任何程式都是由if/for等基本結構所構成的。但要把任何問題都拆解成test case,並且每加一個test case只多一個判斷,有點太過天真。
  • 寫一個我推測很可能會通過的Test-Case,寫了就過了,是不是就破壞了紅燈、綠燈、重構的原則?
  • 辛辛苦苦花了許多時間,列出完整不同需求情境的Test Case,但列的太細,寫到一半發現許多太細的test case都沒用了,結果產生了浪費。
  • TDD似乎有種命定論的味道,一開始想的演算法如果不漂亮,那實做出來的程式也不會太簡潔。TDD有辦法協助演算法設計嗎?
  • 如果一個獨立的Test Case太大,需要幾個method才能完成,那是否要將這些method暴露成public方法?理想上是不要,所有的public method測試應該就要涵蓋到private method的行為。可是一個物件的private method較為複雜的時候,很難透過public method的測試小步小步的開發private method。
  • ......

有些偏向觀念上的問題,我就丟到課後群組裡詢問。有些問題因為自己還沒有切膚之痛,問了沒太大意義,就先放在心上,等到經驗多一點時,自然就有了自己的見解。

後來,我又練習了MineSweeper、練習了Game of Life,找另一個朋友pair了programming Bowling。這些都是網路上找得到的範例題目,但這些Kata還是讓我很不踏實,我常常會覺得自己寫得不如網友的練習影片。我甚至不確定我能不能用TDD開發一個非範例的程式。

後來過年,神來也麻將出現在每個youtuber頻道的置入,我下載了麻將遊戲來玩,把一開始給的遊戲幣都輸光後。我想到可以練習用TDD寫一個判斷聽牌的程式。如果我能把這題用TDD寫出來,就代表我已經算是踏入TDD的世界了。

我從頭開始分析需求、結果才剛開始我就放棄了,太多決定讓我思考癱瘓,我不確定我的作法符合TDD的原則、我的方法似乎不該回傳List of Reference,這絕對是有問題的。我發現我對C#還不夠熟悉。

我意識到需要對C#有更深的掌握。於是查了C#的相等性和比較、看Collection和Enumerable、看看C#是怎麼定義介面的功能。掌握這些知識後,我意識到更進階的問題:我對好的物件導向設計不夠有Sense。物件導向的六大原則我們都會背,封裝多型繼承誰不知道?但真正的問題是,平常根本沒有人告訴我們這是好的設計、這是不好的設計,大家都是悶著頭寫程式,寫完能動就算了,這樣要怎麼進步?

運氣非常好,我發現一個很棒的blog,有位外國人在Udemy開了一門線上課叫做Emergent Design。我看了幾部預告後,就決定花錢買這門線上課,花了好幾天反覆的看。裡頭用一個開發數字計算遊戲的案例程闡述設計的原則。他沒有使用TDD,並把開發過程稱為Iterative Development,我一看就就知道他的觀念和TDD是相通的,更重要的是,他補足了我在TDD學習之路上還沒被填起來最大的那個坑,設計。

Emergent Design做兩件事,一個是小步開發(Develop Iteratively),另一個是持續設計(Design Iteratively)。每一次開發,永遠只處理當前最直接的問題,並把更小的問題委託給另一個物件解決。影片中大量使用了LINQ,並且闡述了Lazy Evaluation的潛在問題,我發現我只會幾個基本的LINQ的語法,常常需要把影片暫停,去查語法,再繼續看下去。我才意識到,原來LINQ是C#的精華。於是我又花了一段時間,整理並練習了LINQ的所有API,至少要讓自己在遇到問題時,知道手邊有這個工具可以用。

我把影片反覆看了好幾次,不懂的地方就暫停。裡頭提到許多相當實務的設計思維,像是把Domain Code與Common Code分離,不僅僅是為了Code Reuse,更大的好處是你需要理解的Code Size會變少。儘管他沒有用TDD,但是依然遵守著類似TDD的原則,在不破壞程式先前的功能下加入新功能。抽象應該是自然而然演化出來的,而不是直接一開始寫死的。如果不這麼做,你必須腦袋時時刻刻保持著你幻想的抽象概念,寫Code時還要驗證該抽象是否合理,很容易會造成自證預言式的設計。

我們很常用蓋房子來比喻軟體的設計。把規格拆成一個步驟一個步驟做,先打地基、架鋼筋、之後接著灌漿,如果我要求你改變房子的設計,修改就會變得很棘手。好的設計應該像是用樂高積木一樣,當需要修改時,只要抽換掉要修改的積木就可以了。

我從線上課程中收穫良多,終於,我覺得自己已經知道大部分的工具。開始試著用TDD寫判斷麻將聽牌的程式。這次比之前都順利,我開始能夠判斷作法的好壞,儘管不到流暢,但終於能夠完成整個練習。我開始體會TDD的美麗之處。TDD的本質不是單元測試,而是強迫開發者及早面對「設計」的問題。透過重構、決定哪些方法要放在同一個類別,實現bottom-up的設計,透過emergent desgin和測試,預先思考一個類別的職責,實現top-down的設計。提早思考函式介面的設計、分離意圖和實做的差異。當整個程式開發的每一步都照著TDD原則走,等於每次寫程式都像打一套詠春。練習久了,把心法和招式內化,日後出手自然就知不同。

剛上完91的課時一直不敢寫心得文,課程本身很棒,指出了學習的正確路標,但內化技能需要時間、練習及填坑,把頭洗下去說出的話比較有說服力。路還很長,先把這段心路歷程做個記錄,希望能幫助想入門的朋友少走一些彎路。另外有哪間公司或團隊有在工作上使用TDD或寫單元測試的,拜託留言或私訊讓我知道,我準備開始找工作啦!