【單元測試】改變了我程式設計的思維方式
單元測試(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] IEEE Standard for Software Unit Testing |