談到測試,許多開發者的心總是糾結的,寫測試需要時間,有時花的時間比寫被測的程式還多,寫測試有難度,特別是被測對象沒有提供適當的工具或掛勾可供測試之時。寫測試也需要思考,有時根本不知道應該測什麼東西,或不該測什麼,最後淪為遵守教條,為了測試而測試。

從測試自動化開始

談到測試與教條,開發者多半會想到的就是測試驅動開發(Test-Driven Development, TDD),在《測試驅動開發-使用Python》第一章開頭,就強調出測試驅動的第一紀律:「服從測試羊!除非你有測試程式,否則別做任何事情」,如果從沒試過測試驅動,可以試著跟上書中的每個操作,到後期程式變得複雜時,就會感謝有測試的存在。

測試驅動的嚴格紀律,也帶來了不同評價,最有名的一次戰爭,莫過於Uncle Bob、Kent Beck等大師,摃上Rails創建者David Heinemeier Hansson所發表的〈TDD is dead. Long live testing.〉(https://goo.gl/ci3xzO)。無論如何,這表明了,想要遵守測試驅動的嚴格紀律,需要有一段正確學習、訓練與習慣的過程,嚴格遵守紀律是有價值的,特別是涉及到軟體開發,而不單純是程式設計之時,畢竟〈程式設計不等同於軟體開發〉(https://goo.gl/qNMxHO)。

回歸到自然一點的角度來看,有時我們只是想單純享受一下程式設計的樂趣,從一個隨意撰寫的原型開始,逐步驗證想法再演進程式,會想要先撰寫測試程式嗎?可能不會,然而,這不表示程式不需要測試,也不代表程式的撰寫過程中,不會進行任何測試。實際上,幾乎不會有一名開發者寫完程式不動手測試的,就算是個Hello, World之類的,也會看看有沒有正常顯示、跳出視窗之類的吧!

手動地測試每個人都有過,也就是說,就算不採測試驅動,程式仍是需要測試的,然而,就算是從隨意的原型開始演化出來的一人專案,也可能會開始日趨複雜,手動測試變得重複、繁瑣,因而引發出耗費時間、容易遺落測試案例等問題。因此,如果發現到某個頁面需要按十幾次確定按鈕來關掉警訊方塊,還有什麼理由去拒絕自動化這個過程呢?

我該怎麼測?

有的!很多人會拒絕測試的理由之一是,不知道有什麼工具可以使用,然而,這不是正當的理由,就現今主流平臺、語言,或者一些受歡迎的程式庫、框架來說,多半都能找到可用的測試工具,像是《測試驅動開發-使用Python》中,就談到了unittest、Selenium等,而Django本身也提供了一些測試掛勾(hook)用的API,只要略為調查,在工具方面想要有個起點,應不是難事。

很多人會拒絕測試的理由之二是,不知道怎麼針對目標程式寫測試,實際上,如果不知道該怎麼寫測試的時候,才更要寫測試,這給了程式碼一個重構的好機會與目標——在正式寫測試之前,先試著整理程式碼,重構的過程中,得不斷地問自己,對於看似無法測試的模組來說,之前手動時到底是想測出什麼?逐步地將想測試的程式碼集中,甚至抽取出子函式或模組,程式碼的意圖就會被突顯出來,程式意圖一旦清楚明瞭,就會知道該測試什麼、不該測試什麼,以及該如何測試。

舉例來說,3D建模時該怎麼測試模組?模組是一個有副作用的輸出,OpenSCAD本身並沒有測試用的掛勾,而針對匯出的模型檔案(像是STL)是不切實際的,因為動輒數十MB且難以事先準備測試材料(fixture),在試著為模組加入測試的過程中,我總是先重構,對於抽取出來的子函式或模組發問:「這是我想測試的對象嗎?我該對它測些什麼」。

子函式往往是沒問題的,OpenSCAD的函式必須是純函數式,因此沒有副作用,一個輸入只會有一種輸出,非常適合測試,這樣我就在一個原本看來無法測試的模組,找出可測試的部份了,往往地,發掘出來的子函式越多,可測試的比例就越高,最終不可測試的部份,就會被擠壓到某個邊界。

如果不可測試的部份已推到須呼叫內建模組的地步,可能就不需要測試。搞清楚測試的邊界很重要,不需要測試你所採用的程式庫或框架之功能,只需要測試傳給內建模組前的引數是否符合預期。例如,在OpenSCAD,不用測試內建的polyhedron是否正確繪出多面體,而是要查看那些演算出來預計傳給polyhedron的座標點,是否符合預期。

因此,對於一些顯而易見的模組,也是不需要測試的,例如底下這個hexagon模組,因為circle是內建模組,轉動30與邊數為6都是固定的,簡單來說,這不過就是個膠合用的模組,不需為了測試而測試:

module hexagon(r_hexagon) {
rotate(30) circle(r_hexagon, $fn = 6);
}

認清測試之目的

往往地,認清「不」該測試什麼,跟知道該測試什麼同等的重要,這可以避免為了測試而測試。在〈The tragedy of 100% code coverage〉(https://goo.gl/YaBTsF)中,也看到類似的說法,作者談到在辦公室曾對某個開發者說過,某個方法不需要寫測試,因為「程式碼顯而易見,沒有條件判斷、沒有迴圈、沒有轉換,什麼都沒有,這程式碼不過是簡單的老膠合程式碼。」

將一個無法測試的程式碼進行重構,從而使之能夠進行測試,實際上也是測試的目的之一,也就是「讓程式碼變得可測」,這除了能提高自動化測試的比例之外,過程中也會發現一些事先沒考量到的問題,未來在修改程式碼時,也才能保證程式結果沒有被破壞,一切仍如當初預期之方式運行。

這也說明了另一件事,測試並不是保證撰寫出來的程式碼是正確、沒有臭蟲的,只是證明程式碼符合預期,有句老話說:「程式是照你寫的跑,不是照你想的跑」,測試可以說只是用來確定「程式也是照你想的跑」,然而,想的卻不一定是正確、無臭蟲。

換個角度來看,如果已經有段可運作且符合預期的程式碼,然而還沒加上測試,可試著為它加上測試,未來若程式之需求有了變動,也才能確保相關程式碼之行為,甚至是測試程式本身,都有考慮在修改的清單之內。

如同一般的程式碼,測試程式碼也需要維護,未來若預期的行為修改,測試程式碼也要跟著修改,有時在修改測試程式碼時,為了要思考預期行為是否被確切地描述,花費的時間甚至可能更多。

花費的時間換來的是未來的防線,在《測試驅動開發-使用Python》中後期的案例中,就有不少需求變更,因為有測試作為後盾,使得一些沒被考慮到要一起修改的部份,因為測試失敗,而被提醒得去修改功能程式碼,或者是測試程式碼本身。

建立測試的掛勾

無論是測試驅動開發,或者是後來才加上測試,將不可測的程式變得可測,實際上,都有個重要的目的,為程式建立測試的掛勾,為未來留下測試的可能性。

如果是在開發程式庫或框架,提供測試的掛勾尤其重要,例如Django在這點做得很好,像是提供了LiveServerTestCase,可自動清理上次測試遺留下來的項目,就增加了開發者撰寫測試的意願。

如果使用的程式庫或框架,沒有提供適當的測試掛勾,試著從加入測試、不斷地整理、重構、思考該測些什麼的過程中,建立起自己的測試掛勾,甚至是專屬的測試框架,不但可以使得程式更為可靠,對於未來在演化、添增新功能時,就更能在一開始,將撰寫出可測試的程式作為優先考量,也許到那時,測試這件事本身也會變得自然,甚至能從中享受擁有測試的樂趣。

專欄作者

熱門新聞

Advertisement