前言
寫單元測試常碰到的兩種問題:
- dependency 的問題,導致無法隔離相依,無法模擬或驗證相依對象的互動
- 要模擬的相依對象太多
其實,這都是散發著壞味道的象徵。
本文
當在撰寫 isolated unit test 時發現,要 stub 的對象太多時,往往是兩種 bad smell 的徵兆。
- 單一職責的另一種 anti-pattern, 把一個職責過分拆細,導致要完成某一件事得組合太多零碎的相依對象才能運作。(比較常見)
- 測試目標對象 (SUT) 的職責過大,需要很多相依對象互動才能完成自己的職責。(機率較小)
另外要留意的一個要點,如果假對象都是 mock, 而非 stub, 那通常代表用錯了。
mock 是透過注入假對象,用來驗證測試目標與這個相依假對象之間的互動,也就是 assertion 的目的。而 stub 對象是用來模擬相依對象的動作,以便讓單元測試能獨立驗證測試目標對象本身的邏輯是否正確。
因為單元測試一次只測一件事,所以請勿同時對測試目標對象進行 assert, 又對 mock 對象進行 assert。因為這兩個 assert 的關注點,勢必是不同件事。
在《單元測試的藝術》一書上提到,mock 跟 stub 在實務上的比例大約是 5%:95%(個人經驗差不多是 10%:90%)。過多過細的 mock, 會讓 test cases 的穩定度 (Robustness) 下降, 孩子生了就得養,所以需要生的時候再生就好。
剛好的 mock, 可以有效降低測試用例幾倍的維護成本。
如果你非得驗證到 mock 對象互動所接收到的參數,請記得「只驗證意義,例如透過 regex pattern。而不是驗證參數的所有細節」
補充
測試用例的撰寫與維護,往往是 production code 設計品質的照妖鏡。如果你碰到以下的狀況,代表:
- 一個需求異動,要加好多的測試用例:代表違反單一職責與開放封閉原則,應該只需要新增 class 做切換,而不是對原本內容修改。
- 一個需求異動,要改好多測試用例:代表違反單一職責,一個對象的職責太大。
- 要 stub 的對象很多:代表違反單一職責,因為同一個職責被拆到太多對象身上,要做一件事就需要很多個對象一起,才能正常運作一件事。
- 互動的方式一改、參數一改,測試用例就壞:代表 mock 太深,mock 可以只驗證互動次數、參數的意義以及參數的完整內容,越後面代表綁得越深。
- UI 一改,所有測試用例都要跟著修改:layout 與 scenario 耦合性太高,請透過 page object pattern 從測試程式設計上解耦。