我想學 TDD 來讓我《更有自信的寫完程式及重構》

因為最近發現了 Essential Developer 這個教學課程,會得知是因為先前他們有為期一周免費的 iOS Architect Crash Course 課程(題外話,裡面有 Bonus 的課程是找一位他們的學生出來發問並且深入討論,重點那位學生是 CocoaHeads…

我想學 TDD 來讓我《更有自信的寫完程式及重構》
Photo by Alvaro Reyes on Unsplash

因為最近發現了 Essential Developer 這個教學課程,會得知是因為先前他們有為期一周免費的 iOS Architect Crash Course 課程(題外話,裡面有 Bonus 的課程是找一位他們的學生出來發問並且深入討論,重點那位學生是 CocoaHeads Taipei 的主辦人假企管真資工?✨),雖然只有短短一周但是裡面的技術含量非常高,感覺像是挖到金礦一樣,上完之後我非常興奮去到了他們的網站發現他們有一系列免費的課程就是在教 TDD,所以我就下班的時候開始看 How to Build iOS Apps with Swift, TDD & Clean Architecture 雖然我還沒看完,但是打鐵要趁熱我就趕快先來分享心得,以及我目前所認知道的部分。


什麼是 TDD 呢?

測試驅動開發(英語:Test-driven development,縮寫為TDD)是一種軟體開發過程中的應用方法。
測試驅動開發, Wikipedia

根據維基百科的說明,我們可以得出它其實是軟體開發的其中一個方法,然後由測試為引擎來驅動整個開發過程,所以就是先寫測試後開發

既然有測試驅動是不是代表也有別的驅動?沒錯除了 TDD 以外還有像是 BDD、ATDD 等等,不過已經超出本篇的範圍了就不討論,有興趣的可以去深入瞭解下。

整個流程其實不複雜,用 RGB 這三個顏色(紅燈停、綠燈行、藍燈不要停)代表了不同的流程:

TDD 的開發流程

1. 紅燈:先寫一個會失敗的測試。

You are not allowed to write any production code unless it is to make a failing unit test pass. 
The Three Laws of TDD, Uncle Bob

這個第一步非常重要!你可能心裡會想直接寫一個通過的測試不行嗎,寫失敗的測試是為了確認測試能夠正確地檢測到錯誤,畢竟不會失敗的測試還算是測試嗎?所以能失敗的測試也代表著測試是有效的。

這個階段我們不能還不能實現 Production Code 的邏輯,只能最能限度的建立讓編譯器順利通過即可,以下面為例這個 sum(x, y) 為了讓編譯器通過只先回傳 0,那測試就不會通過了。

// Test Code 
XCTAssertEqual(add(5, 8), 13) // 🔴 
 
// Production Code 
func add(_ x: Int, _ y: Int) -> Int { 
 return 0 
}

2. 綠燈:只寫剛剛好會通過的程式,這裡是精髓非常重要!Write Just Enough Code 用最笨最簡單的方式(KISS 原則)去寫就好,因為這樣寫出來的程式碼就會真的剛剛好,不會 Under-Design 或 Over-Design,因為這個應該很多工程師都有過這樣的經歷,寫的太入迷時就會將這支程式變得十八般武藝樣樣精通,不過這樣就不符合了 SOLID 中最重要的 SRP 原則。

KISS 是代表 Keep It Simple, Stupid,意思就是保持簡潔以及單純即使這個方法很笨。
// Test Code 
XCTAssertEqual(add(5, 8), 13) // 🟢 
 
// Production Code 
func add(_ x: Int, _ y: Int) -> Int { 
 return 13 
}

3. 藍燈:最愛的部分就是這裡了,可以放手去重構了,前兩步我們已經將測試寫好了,現在開始我們只要改過的程式有問題測試就不會通過,消除改了程式後怕不知道哪裡又出問題的煩腦 Be Confident!

// Test Code 
XCTAssertEqual(add(5, 8), 13) // 🟢 
XCTAssertEqual(add(1, 2), 3) // 🟢 
 
// Production Code 
func add(_ x: Int, _ y: Int) -> Int { 
 return x + y 
}

你寫程式有測試嗎

這一定都有的吧,只不過用的測試方法不盡相同,又或是測試的角色不是你,總之我們的程式都會經過一定程式的測試才敢交付。

你的流程大概是這樣,重複 2 ~ 3 直到問題被修複:

  1. 確認問題
  2. 找到問題程式進行修改
  3. 執行程式到此區塊,並確認問題是否被修複

這個就是你手動的進行測試,這是完全沒有問題的,但是今天你把 A 問題修改成功後,改天修 B 問題你要怎麼確保 A 問題依然是正常的呢?

全部一起檢查不就好了!這是正確答案,不過身為工程師就是為了解決問題存在,身為懶惰工程師就會希要能用越簡單越少的時間完成同件事,所以才會有自動化測試的出現,這些手動測試的時間累積起來是很可怕的。

常見的開發團隊測試流程

  1. 先寫測試後開發:遵循這個開發流程的話,能提升程式品質(只是其中一種提品質的方式)
  2. 先開發後寫測試:可以提高開發速度,但有可能會遺漏測試,在 Junior 身上還沒有足夠的內功,可能會還寫出耦合高的程式碼,如果團隊內的開發人員能力差距大參差不齊亦或是流動性高,長期可能導致維護成本增加。
  3. 不寫測試:不建議,因為無法保證代碼質量和可靠性。

以上沒有絕對的好或壞,依據團隊情況做選擇,如果開發成員都是不管怎麼寫都是高品質程式碼,那麼或許他們可以先開發後測試,因為他們不需要經由開發來協助自己寫出可測試性高的程式,這些通通已經深深的烙印他們的腦海裡了;相反若是功力相差很大,那先寫測試後開發就能讓程式有一定程式的品質。

TDD 可以幫助我解決什麼?

開發前先思考,它幫你先踩著煞車讓你先試著想想使用情境,並且瞭解該如何使用,降低你一拿到規格就興奮的去打 Code 不經思考的衝動。

  • 它可以幫你寫出剛剛好的程式碼,依現有需求開發,不會過度設計。
  • 它可以讓你的程式至少有一定的品質,未來的人要再接手修改也不會太困難。
  • 重要的是可以讓你更放心的去重構,避免技術債越堆越高,然後都沒有人敢去動!

有感受到重點了嗎,它主要是無形中提供你保護網讓你寫的程式會在一定範圍內,真正要寫好的程式還是有很多方面需要去加強的,不是一個 TDD 就能打天下。

我喜欢把编程比喻成开赛车,而测试就是放在路边用来防撞的轮胎护栏……
优秀的车手会很快看见优雅而简单的路径,恰到好处地掌握速度和时机,直奔终点而去。护栏只是放在最危险的地段,让你出了意外不要死得太惨。
测试的道理, yinwang

我很喜歡這篇文章舉的例子,非常建議去讀,作者有不同的角度去分析以及分享自己的親身經歷。

測試那麼好的話為什麼還是很多團隊不寫?

成本考量

沒錯就是這樣,這個以公司的角度可以簡單來分析,因為常常專案的要趕死線,不一定都能準備交付了所以要生出時間更是難上加難,公司的目地是賺錢,要多花時間賺的錢還不會變多,那當然就不願意啦。再來真的有時間可以來寫測試那也是佔用公司的資源,變相的公司要多花錢寫一個不能直接看到效益的事。

當然以我們技術團隊的角度來看,長遠來說我們在專案上花的時間不敢保證會變得多短,但至少能夠減少後續花在 Debug 的時間有指數級上升的可能(多個程式之間的關聯性越來越緊密就有可能),我們也可以省去不少時間去檢查自動化測試就能做好的事,人工測試也可以放在更重要的地方。

那所以測試是什麼?

我們先想想要怎麼證明你的程式寫好了?

:「我寫好了,我也測好了」

在職場上這個大家也都會常這樣聽到吧,但是從不同人口中說出這句話在我心中確是不同結果,因為每個人的功力不同所以這個是很主觀的表達完成了,當你只侷限在你所接收到的任務上那肯定是沒什麼問題,那問題時你能想到這個任務的程式做完後會不會對別的地方有影響?你會想不到的原因有很多,像是你可能是新人對這個專案不熟,或是剛還始入門還程式的思維還不足讓你想到這些。

所以,寫測試就變成一個很客觀的評斷標準,在你還沒熟悉專案以及商業邏輯,那你會很高興有測試幫你檢查這些,你就可以專心聚焦在現在的任務身上,不小心改壞了什麼地方你也可以用最少的時間得知。

所以至少對主管/技術團隊有這個標準可以衡量,不過這個終究都是給技術人員看的,那如果要給客戶看的話怎麼辦,那就是要給他們看得懂的測試,最常見的就是開 Word 檔加上截圖與文字說明,或是跟 TDD 有異曲同之之妙的 BDD(行為驅動開發 Behavior-driven development),這個部分我還不瞭解,有興趣的可以再去深入研究。

我要開始寫測試了,需要注意什麼呢

我們是為了什麼寫?是為了測試程式的邏輯對吧,所以不要再測試中又添加更多的邏輯,簡單說就是能夠判斷並產生分支的語句,例如 if elseswitch 等等。

我們這篇主要是寫單測試,所以範圍盡量縮減到這個單元本身,聽起來有點饒舌,我隨便舉個例子,譬如今天我要測試的是 drawRectangle 這個方法,需要傳入 x, y 座標以及 width, height 長寛就能產生一個 Rect 物件。

func drawRectangle(x: Int, y: Int, width: Int, height: Int) -> Rect { 
  // ... 
}

那我今天如果只要測試產生的 Rect 物件的座標是否正確,那我們看下面兩行例子,只傳入 x 跟 y 的物件更加清楚這次要測試的東西,而不會被其他參數去混淆,去讀的時候也更加清晰。

let rectange = drawRectangel(x: 12, y: 18) // o 
let rectange = drawRectangel(x: 12, y: 18, width: 50, height: 30) // x

切記,不要為了測試而盲目的改介面,你要自己判斷怎麼做才是比較好的。


後記

雖然主題是 TDD 不過還是花了很大篇幅在說明測試,畢竟測試才是核心而 TDD 只是實現它的其中一種方法,所以我認為把核心理解後其他不同的實現方法都能更好的掌握。

然後我個人的工作中就是屬於沒有寫測試的那個團隊😭,所以希望透過這次學習完之後能夠導入進我們的團隊中,有任何導入過的朋友們歡迎跟我分享,我非常需要你們的經驗!

如果喜歡我的這篇文章的話可以在下面幫我點個讚👍🏻支持我,我會繼續分享更多內容給大家閱讀。

參考資料