大多時候,我們花許多時間在開發功能,隨著功能越來越多,功能之間也互相影響。有時候,我們改了一個功能,另外一個功能卻壞了,但是我們卻沒發現。如果有 QA 幫忙測試,或許還能透過專業的測試手法找到,萬一今天只有我們自己測試,而我們又沒花太多時間測試,就會把 Bug release 出去了。
為此,我們可以進行單元測試,測試我們開發的功能。當我們每次新增功能,也為他加上測試,這些測試就能保護我們的功能,在我們因為新增其他功能而改壞或者重構有問題時,就會在我們執行時讓我們知道,哪些地方改壞了。
開始測試
在這邊就讓我們測試一下 Day 8 的 SelectedPhotos 類別的 select 方法。
https://dartpad.dev/?id=886bcbddaf299475a141d22e7d099a4e
從這個類別中,我們可以看出 select 有幾個行為,並為其列下測試案例:
- 當 photo 未被選擇,也沒超過限制時,就會能成功選擇 photo
- 當 photo 被選擇時,呼叫 select 會取消選擇
- 當選擇 photo 檔案大小總和超過 250 時,就會丟出 OverLimitException
- 當選擇 photo 超過 5 張時,就會丟出 OverLimitException
測試第一個案例
在 Flutter 中,我們會把測試寫在 main 方法中,並在 main 方法中放入單元測試。在單元測試中,透過實現 3A 原則:Arrange、Act、和 Assert,從準備 photo 資料、實際執行 select 方法,最後確認 photo 已經被選擇。當我們完成每一個步驟後,執行測試就從 IDE 看到一個測試通過了。
https://dartpad.dev/?id=886bcbddaf299475a141d22e7d099a4e
如果讀者們有興趣,也可以自己嘗試第二個測試案例。讓我們跳過第二個,嘗試測試一下非正常的流程。
測試非正常流程
當選擇的總檔案大小超過 250 時,我們執行 select 時,就會拋出一個 OverLimitException。如果我們直接在測試中執行 selectedPhotos.select,測試就會因為 OverLimitException 被拋出而失敗。所以在這測試案例中,我們不能直接呼叫 select 方法。而是必須使用 callback 的方式,把呼叫 select 的工作傳給,expect 方法,並讓 expect 方法幫我們檢查是否 select 方法有正確的拋出 OverLimitException。
https://dartpad.dev/?id=381128d4a7999a0a086055bd61e28bb1
同樣的,如果讀者們有興趣,也可以自己嘗試第四個測試案例。
設定假資料
有些時候,我們測試目標會相依於其他類別,與其他類別互動,最後完成工作。像是下面例子中 NewsRepository 並不會自己打 API,而是透過 HttpProvider 呼叫。在單元測試中,我們希望能避免直接呼叫真的 API 或使用 DB 等外部資源,我們必須做假這些互動。
在這些測試中,我們會使用 mock 套件來幫助我們做假這些互動,讓我們更好測試。在 Flutter 中,我們可以選擇使用 mockito 或 mocktail 來幫助我們做假。在這邊,我們使用 mocktail 來示範。
https://dartpad.dev/?id=73c5804efc8ce14db9b2d7c727ee369e
雖然這個測試與 selectedPhotos 的測試看起來不太一樣,但其實還是符合 3A 原則的。
- Arrange:透過 mocktail 的 API 來做假 HttpProvider 的回傳資料
- Act:呼叫 NewsRepository.get
- Assert:驗證 NewsRepository.get 的回傳資料使否符合預期
測試也需要重構
由於測試方法裡頭充滿了細節,讓我們可能不太好看出測試的流程。所以我們應該也要對測試進行重構,讓測試也具備可讀性。如此一來,當測試失敗時,我們才不會花一大堆時間看懂測試,然後才知道什麼東西出錯了。
https://dartpad.dev/?id=9fb4a07b2877366e73b2c1401dc051ca
當我們重構測試之後,測試也從長長一串,變短一些,也隱藏一些測試實作的細節,讓我們能更專注在測試的流程上。
當測試很難寫時
有些時候,我們會發現我們很難進行單元測試,有些時候是需要 mock 太多東西,有些時候是為了測試,需要準備很多資料。其實當我們發現測試很難寫時,也可能表示類別的設計有問題,可能是職責太多,也可能是直接使用了靜態外部套件,這些問題都會讓我們測試時遇到很多困難。
所以當我們發現不好測試時,應該適時的檢視當前設計,看看是否應該把類別的職責再拆小一點,或者其他各種方式,提升程式的可測試性。
結論
單元測試看起來雖然簡單,但其實並不容易。我們在這篇文章中,簡單介紹了測試時會需要的東西,並未討論深入討論各種關於單元測試的知識,例如:各種測試替身,和如何設計測試案例 …等。關於這個議題推薦大家去閱讀各路大神的文章,了解更多關於單元測試的知識。