大多時候,我們花許多時間在開發功能,隨著功能越來越多,功能之間也互相影響。有時候,我們改了一個功能,另外一個功能卻壞了,但是我們卻沒發現。如果有 QA 幫忙測試,或許還能透過專業的測試手法找到,萬一今天只有我們自己測試,而我們又沒花太多時間測試,就會把 Bug release 出去了。
為此,我們可以進行單元測試,測試我們開發的功能。當我們每次新增功能,也為他加上測試,這些測試就能保護我們的功能,在我們因為新增其他功能而改壞或者重構有問題時,就會在我們執行時讓我們知道,哪些地方改壞了。
在這邊就讓我們測試一下 Day 8 的 SelectedPhotos 類別的 select 方法。
https://dartpad.dev/?id=886bcbddaf299475a141d22e7d099a4e
從這個類別中,我們可以看出 select 有幾個行為,並為其列下測試案例:
在 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 原則的。
由於測試方法裡頭充滿了細節,讓我們可能不太好看出測試的流程。所以我們應該也要對測試進行重構,讓測試也具備可讀性。如此一來,當測試失敗時,我們才不會花一大堆時間看懂測試,然後才知道什麼東西出錯了。
https://dartpad.dev/?id=9fb4a07b2877366e73b2c1401dc051ca
當我們重構測試之後,測試也從長長一串,變短一些,也隱藏一些測試實作的細節,讓我們能更專注在測試的流程上。
有些時候,我們會發現我們很難進行單元測試,有些時候是需要 mock 太多東西,有些時候是為了測試,需要準備很多資料。其實當我們發現測試很難寫時,也可能表示類別的設計有問題,可能是職責太多,也可能是直接使用了靜態外部套件,這些問題都會讓我們測試時遇到很多困難。
所以當我們發現不好測試時,應該適時的檢視當前設計,看看是否應該把類別的職責再拆小一點,或者其他各種方式,提升程式的可測試性。
單元測試看起來雖然簡單,但其實並不容易。我們在這篇文章中,簡單介紹了測試時會需要的東西,並未討論深入討論各種關於單元測試的知識,例如:各種測試替身,和如何設計測試案例 …等。關於這個議題推薦大家去閱讀各路大神的文章,了解更多關於單元測試的知識。
以前總會覺得測試只是一個重構時,確保程式行為一致地保護繩。但是越到後來,反而更覺得測試是一個程式思路與品質的照妖鏡,讓我了解這樣寫是不是好的。最近上課,也學到測試也是一個保存當前理解的知識的載體,讓我們知道為什麼要這樣寫。真的是一個很有趣的過程。