【單元測試】改變了我程式設計的思維方式 by pcbill | CodeData
top

【單元測試】改變了我程式設計的思維方式

分享:

單元測試(Unit Testing) 顧名思義,就是以程式中最小的邏輯單元為對象,撰寫測試程式,來驗證邏輯正確與否。一般來說,程式中最小的邏輯單元就是函式(function),或是方法(method)。它不是新觀念,早在1987年,IEEE就把單元測試納入美國國家標準[1]

本文不是要條列出單元測試的優缺點,然後告訴你要如何去實行它,或是放棄它。而是提出在我每日的編程生活裡,引入單元測試後,它對我的影響,以及如何改進提升我的寫碼生命。在進入正題前,我們先定義何謂"目標程式“,它指的是單元測試要測試的程式目標。

根據時機點,撰寫單元測試分兩種類,一是目標程式不存在前就撰寫單元測試,也就是所謂的 TDD (Test-Driven Development),或 BDD (Behavior-Driven Development)。這是另一個題目,下個段落再來討論。現在要談的是另外一種,目標程式已經存在,然後撰寫單元測試的情況。

試想一個情境,當有一個同事離職了,老闆交辦要開始維護他所留下來的程式,時程上不太允許重寫程式,老闆也不會答應。那要怎麼開始的呢?先看文件?然後再閱讀程式碼,一邊碎念,一邊感嘆自己的悲情?假以時日後,才漸漸懂得如何運用前同事的程式?在這過程中,需求還是不斷進來,必須對原來已經穩定的程式加以修修改改,一不小心又藏了一隻 bug 。然後 bug 爆發,老闆就會質疑原本好好的程式怎麼會出包,最後開會被釘到牆上。很熟悉吧,也很無奈,難道這是程式員的宿命?

所以一般的流程是先讀程式碼,了解程式碼後,才去運用。其間也沒有其他工具輔助,頂多畫畫流程圖。然後又換人接手維護,再一次進行前述的苦情循環。現有的開發環境和工具對這情形是沒有多大幫助的。但是一直等我遇到了單元測試,這情形就改觀了許多。

當我拿到像一鍋粥的程式碼,我不會馬上跳進去和它們攪和一起。我會先快速掃一下,依據經驗找出程式壞味道(bad smell),針對這壞味道所在的目標程式,先進行單元測試撰寫。我會針對我對目標程式介面的認知,撰寫我自認為正確邏輯的測試。然後測試它,如果測試 pass ,就是我的認知符合我對目標程式的期待,並且把這樣的認知,透過單元測試寫了下來,確認了這樣的"規格";如果測試 fail ,就是我的認知和程式行為不一致,要不是程式有問題,就是我的認知是錯的。此時我才會跳進目標程式碼,作細節探究。假設是我的認知錯誤,則修正測試程式;反之,就是目標程式的問題,就小心的修改目標程式,直到測試 pass 。

這樣的過程,不僅只有探索,而且記錄了結果。這還可以作為日後的回歸測試的依據。

這樣程式設計的思維方式從原本的讀碼 -> 了解 -> 使用,轉變成讀碼 -> 使用 -> 了解,僅是後兩步驟順序互換,就會帶來非常不一樣的效果。寫程式時的思維,會由原本的先了解程式細節切入如何使用,轉變成觀察使用的程式介面,探索如何使用。這有一點退一步觀賞的意味,不管是正在欣賞的是藝術作品,還是密密麻麻的程式碼,道理是相似的。這樣就不容易當局者迷,墮入程式碼的五里霧中,分辨不清方向。它也促使你重新思考這樣的設計是否合理,繼而考慮是否要重新設計。

更進一步來說,如果探索學習的是第三方程式,或是大家愛用的函式庫、開源專案。除了讀官方文件(不見得完備)外,是否還有其它的學習使用方式?其實,這些函式庫就是寫得比較好的"目標程式"。雖然不見得有源碼可以修改,但是學習如何使用,同樣可以套用單元測試的方式。就是讀碼 -> 使用 -> 了解。但是因為有一些函式庫不見得容易測試,所以可以在這些函式庫外,再套一層方便測試的 Adapter Pettern。而這些 adapter helper class 可以納入目標程式中,這樣底層就可以很容易抽換不同的函式庫實作,作到低耦合的良好設計。

TDD (Test-Driven Development),或 BDD (Behavior-Driven Development) 的開發方式,我戲稱為"許願式程式設計“。因為在撰寫目標程式前,目標程式不存在的情況下,就要先寫這目標程式的單元測試,感覺很像願望還沒實現前,先許願一樣。當目標程式依據單元測試撰寫完成後,願望也就成真了。

現行的開發順序,通常是寫碼 -> 測試 -> 重構[2]後,再一次同樣的循環。許願式則是測試 -> 寫碼 -> 重構,就前兩步驟順序互換。如果你先許願,然後去實現它。你會發現結果只會實現你許過的願望,不會發生實現你沒許過的願望。以軟體開發術語來說,就是花最少力氣完成目標程式的開發,剛剛好符合單元測試的"規範"。絕對不會過度設計(over design),而且寫出來的目標程式碼 100% 可以被測試,測試涵蓋率百分百。這是以開發流程天性(nature)來保證軟體開發準則,這樣比較可靠,且省力氣。

既然單元測試這麼神,怎麼不見大量流行?就我觀察有幾點。首先,它不像使用他方函式庫馬上有效益,可以看出成果,使得專案管理階層不會重視,也不會視之為專案的產出。還有開發人員對單元測試工具的不熟悉,撰寫有效的測試程式也需要有一定的規則遵循和學習,所以有相當的引入成本。相關的工具、平台往往也沒有那麼完備,要進行有效的測試並沒有那麼容易達到。

天下沒有完美的銀彈(Silver Bullet),單元測試能夠涵蓋的範圍也僅止目標程式的一個單元,它無法發現整合錯誤、效能、或者其他系統級別的問題。它也需要和一些機制整合,才能發揮它的綜效,一是自動整合測試,引入系統自動測試、輸出報表、主動告警通知,減少引入單元測試後這些額外的成本;還有程式碼版本控制,建立程式碼變更歷程,這樣才能方便一發生目標程式修改後,單元測試出問題,無法短時間解決時,快速倒回穩定版本,或是另建分支進行大膽修改測試。

那如何開始進行測試之道?如果你是資訊主管,請馬上建立整合測試環境,並把你手下的工程師送去上課,因為單元測試的第一線把關者就是開發工程師。如果你是苦情的開發工程師,那你不是開發新專案,就是維護專案。開發新專案者,就採用 TDD ,就開始許願吧,這方面的資源很多,不管是線上文章還是書籍,我就不再贅述;維護專案者,千萬不要盡信前輩寫的技術文件,這些文件有可能沒有隨著時間同步更新,真正有效的還是專案的程式碼,所以開啟你的測試環境,開始探索你的專案吧。

我隨時隨地實行撰寫單元測試了一段時間後,對於我維護的專案,除了更了解其功能細節外,只要我鑽研過的細節,都留下一些單元測試,再配合自動持續整合(continuous integration)。日後,同樣的地方再修改重構,我只要花幾分鐘時間,我就可以很有自信(confidence)確認剛剛的修改是否有改壞程式,以保持原來的正確性。這些及早發現的錯誤,如果能及早修正,就節省了日後更多 debug 的時間和成本。這樣一來對於專案的穩定度就可以量化,並可以評估可靠度。帶來的好處是可以準時下班,節省下來的時間就可以用來陪陪家人,或是發展其他嗜好,這不就是每個人想追求的幸福人生嗎?

單元測試是程式開發的一種方法論,所以觀念上適用所有的程式語言。近期,這些測試工具發展亦趨成熟,由於我維護的專案是 JAVA 開發,所以僅列出我目前有在使用的工具或函式庫。

  1. JUnit 4:這算是 JAVA 界最泛用的測試平台了,意思是有許多其他的測試函式庫都以它為本,繼續加入新功能。許多 IDE 都有內建。JUnit 3 就不要考慮了,此處使用 JAVA annotation 比較簡潔,也不要再堅持甚麼 1.4 相容了。
  2. Mockito:個人認為比 EasyMock 好用,是我用來 mock 相依物件的主要函式庫。
  3. PowerMock:因為是維護專案,想得到或想不到的程式寫法都會發生,所以此函式庫主要用來 mock static、private field 或 method ,補 Mockito 的不足。逐漸的,專案的演進重構後,它就可以慢慢退場。

下回我們就實際看程式碼,看我如何運用上述工具實作單元測試吧。

[1] IEEE Standard for Software Unit Testing
[2] 也就是修改程式,不過重構有幾個重要的原則,是個大題目,另開闢文章再來談論。

分享:
按讚!加入 CodeData Facebook 粉絲群

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

關於作者

任職過生醫資訊業、遊戲業、電信業。遊牧於大公司、小公司、半公家機關,所以體驗過各式各樣的開發流程,深信軟體開發應有最佳實踐可以遵循。所以正在努力的許願、追尋中。

作者其它文章

熱門論壇文章

熱門技術文章