淺談單元測試 - 簡介篇
從 2014 年開始學習單元測試,一直到現在也很多年了。這之中常跟同事和朋友在討論,但還沒有認真地寫過心得文。
今天就來寫篇我所知道的單元測試吧!
用在做功能測試
很直接的想法,單元測試可以對測試目標(SUT)做功能測試(functional testing)。
比方說,下面是一個兩數加總的程式:
class Calculator |
單元測試可以使用測試程式,來驗證上面這段程式所實作出來的功能,有符合我們的預期:
public function testSumFunction() |
眾所皆知的,單元測試可以自動化,而且單元測試的執行成本相當低廉,因此一般都可以做到完整驗證。這表示我們有辦法做到每次修改程式的時候,做相較完整的測試。當我們不小心把舊功能改壞的時候,單元測試即會反應這個錯誤,進而達到避免改 A 壞 B 的問題。
但除此之外,單元測試還能幫助我們什麼事呢?思考一個問題:
我們該如何確認寫出來的程式是正確的?
當然方法有很多,以傳統的驗收測試(acceptance testing)或端對端測試(end-to-end testing)來說,是從使用者角度出發的測試。若想驗證上面的程式是否正確,最直接的方法就是開個測試專用的 route:
Route::get('/test', function() { |
這是一個簡單的小程式。寫好後把網址打開,看結果是不是 10,就可以知道程式寫得對不對了。
這個測試方法很直覺,但有幾個缺點簡單描述如下。
無法提交測試程式
正常來說,這段程式不應該提交(commit)到版本控制裡,因為這與實際功能無關。因此若使用這個方法測試的話,每次新的程式或情境,都得重新寫測試用的程式。
無法用在複雜情境
因為上面兩數加總的例子很簡單,所以才有辦法寫出簡單的測試小程式。如果程式呼叫的路徑錯綜複雜,又有很多無法控制的依賴時,是沒辦法用這個方法測試的,只能直接將改好的程式放在實際功能要放的位置,然後再做一次以上的驗收測試。
為什麼要一次以上?因為有可能第一次在測功能的情境一,第二次在測功能的情境二;而不同的情境又需要重新修改程式或調整依賴;因為程式情境複雜就比較容易出錯,測出來的結果通常也不大能直接反應是測試目標的問題。這些問題會導致開發者要「花非常多時間」才有辦法確認「某一小段程式」是運作正常的,這 C/P 值肯定是非常低的。
而單元測試呢?它可以直接測試目標,無視錯綜複雜的呼叫路徑,且結果可以直接反應功能是否正確。
用在做需求整理
在寫單元測試的過程,同時也是在確認與完成需求。
這裡的需求不一定會直接對應到終端使用者角度的需求,有可能只是程式組合上的需求。比方說,數值相加的功能是用在產生報表上,而報表才是真正對終端有意義的需求。在這個情境下,數值相加這個需求應該要長什麼樣子,是沒有人可以問的,只能由開發者自己決定。
可是需求該如何決定,以及確認需求定義沒有問題呢?寫單元測試。
以上面兩數總合的測試程式來說,在撰寫單元測試過程可以思考:兩數相加真的適合在產生報表這個功能上嗎?至少 PHP 上應該是不適合的,因為這樣必須要呼叫很多次函式才能完成一群數字的總合:
$sum = 0; |
這個測試案例會發現,如果我們有「一群數字的總合」的需求,會需要修改程式才能達成。比方說:
// 把 1 + 2 + 3 + 4 改用 array 當參數,這是新需求 |
程式修改結果如下:
public function sum(int ...$items): int |
這裡也可以發現,上面修改程式的流程正好就是測試驅動開發,而且完成後的程式,完整實現了必要的需求。
測試是描述需求的方法之一;單元測試則是描述使用程式的需求。
用在做設計程式
這是我近年來的感想,也是寫這篇文章的重點。
唯有在寫單元測試的時候,才能真正驗收設計程式後的成果。單元測試是用特定的方法與情境來「使用」程式。若用起來不順手,那測試就會很難寫,這時候會是一個警訊,應該回頭來思考程式架構或介面設計上是否有問題。
過多的 mock
比方說,使用過多的 mock,對應的現象是有人抱怨要寫一堆 mock,非常煩人。
這是一種警訊。我個人建議的方向是:能不 mock 就不 mock。雖然它很方便,但終究是假的,我們不能太依賴這些假貨,而是盡可能不去使用。另外,使用過多測試替身,是因為依賴過多造成的;而依賴過多又有可能造成低內聚性,進而需要檢視單一職責原則,「可能」就得重新思考物件導向設計。
單一職責原則需要在特定環境下討論才有意義(參考資料),因此過多的 mock 只是「可能」需要檢視原則,而不是「一定」違反原則。
複雜的斷言
有遇過函式回傳的內容太過複雜,導致斷言(assertion)很難寫的現象。
這有可能是函式本身處理了太多任務,或是依賴了某些副作用(side effect)造成的,如時間。這個問題通常需要做依賴解耦,可以透過依賴反轉原則來達成目的。
小結語
過去的我,對於單元測試的想法只知道有「自動化測試」的好處,在不斷持續寫單元測試後,漸漸才發現:單元測試,不只是測試,同時也是需求與設計!
好測的程式,設計不一定很好;
難測的程式,設計一定不大好。
讓我們一起寫單元測試吧,共勉之!
感謝 @tedmax100 、 @yaochangyu 、 @jame2408 等好友提供許多寶貴的經驗參考。