# Flutter test 初探 - Unit test [TOC] ## 💡 起因動機 首先,我先自白一下,自寫這篇文章的兩天前,我都沒有將TDD (Test-Driven Developement) 套用在任何一個專案上,也許是因為專案內容的關係,也或許是不知道怎麼下手,即使看過了很多有關TDD 優點的文章,又或是看到哪間公司的job requirement 有包括書寫test,都會讓我在看到flutter 專案內的 `test/` 資料夾的時候,對他有種想了解又不知道怎麼下手,這樣糾結的感覺。 直到寫這篇文的上禮拜,我先前的assignment 因為功能缺陷而被R 回來,平常不容易被R 的我,看到自己少寫了條件而被退回,剛開始想的是: - 這麼多條件需求,每個項目都要在手機上測試,怎麼想都怪怪的 - 如果之後又有在這個method 新增條件,我是要親手按到民國幾年? 這時候的我想到了專案根目錄的`/test`,說不定可以趁這個時候來寫看看test,解放手動測試的手指。 ## 📝 測試內容 我負責的是社群app 的貼文邏輯,在貼文時有以下的情境: - 空白貼文 (Not allowed) - 發布文字貼文 (需檢查敏感詞) - 發布圖片貼文 (可上傳1~10 張的圖片,有單圖檔案上限) - 發布影片貼文 (只可上傳1 部影片,有影片檔案上限) 同時在上傳圖片和影片的時候,需要使用走三個步驟: 1. 上傳圖片、影片本身,取得回傳的`token` 2. 使用`token` 傳入api_1,,取得回傳的`token_api1` 3. 使用`token_api1` 傳入api_2,,取得回傳的`token_api2` 最後再將全部處理過後的`<String>[token_api2]` 傳入發文api,完成發文流程。 ## 🧪 測試書寫 在開始講真正的書寫前,先簡單的分享目前理解到的Test 概念 在寫測試時,如果有使用api 的話,則需要模擬一份response。原因有兩個  1. 一個功能內會有多個測試項目,如果使用正式api 的話會很耗時。 2. 如果在測試時使用正式api,若api server 有出現甚麼問題,會讓你不知道是自己的邏輯有錯還是api 回傳有問題。 --- 同時,我們也會有測試的預期,希望這個method 能回傳甚麼、希望這個method 能拋出甚麼例外之類的。也就是說在測試時,我們會有各種假設、也會有各種模擬的response 或是假的資料,來代替真正的api。 以下是使用的dependency: - mocktail - http_mock_adapter - path_provider :::info mockito 是測試用的framework,可以方便我們觀測function 回傳,並根據function的回傳,來假造回傳。 http_mock_adapter 則是dio 的wrapper,用來攔截從dio 發出的get、post,並回傳假資料。 path_provider 是用來在測試時取得asset 的圖片影片。 ::: --- ### 大方向: 測試主結構 在project template 裡產生的`widget_test.dart` 可以看到所有的測試項目是被包在`main()` 裡,而在`main()` 可以再將測試的粒度縮小。最小粒度的測試會是`test()`,而相似的功能的test 則可以再用 `group()`包裝起來,相似的group 也可以包裹group。  以我這裡測試內容為例,我把測試分成兩類group,其中影像貼文還有下分三類 1. 文字貼文 2. 影像貼文 2-1. 圖片貼文 2-2. 影片貼文 2-3. 混合貼文 而在flutter_test 裡,可以在group 內使用 `setUp()`、 `tearDown()`的function,來設定每個 `test()`的運行前和運行後執行的callback,而在 `main()` 還可以用 `setUpAll()` 設定全局、僅會執行一次的callback。 以我這裡的例子,我把api url 跟類似於`WidgetsFlutterBinding` 的`TestWidgetsFlutterBinding.ensureInitialized()` 設定在 `setUpAll()` 裡,並把貼文api 假api 跟攔截放在 `setUp()`。在影像貼文的group 的 `setUp()`裡,放了api_1 跟api_2 相關的假response。 --- ### 預期實體 把大方向寫完後,接下來就要根據實際情況來設定預期。像是當你要測試兩數相加,預期的就是相加的結果;function 的回傳可以是預期正常執行的 `isNotNull`,或是發生例外的 `throwsException`。  `releasePost()` 的回傳值是 `PostResult` 的class,因為我使用了同時static method,我可以很簡單的去預期回傳值   :::info 在寫預期時,是不是易於測試、是不是一個鬆耦合的code,會讓你開始很有感覺,因為當你把程式碼寫的很緊密、高耦合的時候,撰寫測試時就會變得困難。這也就是TDD 會希望在coding 在時候先寫好test,再往後書寫內部邏輯的原因。 ::: --- ### 假資料 from asset  如果假資料是存在於asset 中(json, image 之類的),則需要在setUpAll 或是執行rootBundle 之前使用`TestWidgetsFlutterBinding.ensureInitialized()`,之後就可以像是在android ios 內取用asset。 --- ### mock class  在pub.dev 上有兩個測試套件,我使用的是mocktail。 > 雖然mockito 的publish time 硬是比mocktail 早了5年 (2016 vs 2021),而且mockito 的維護是dart.dev,可以說是官方直營,但我選擇mocktail 的原因是因為 >1. 不需要用build_runner >2. 可以在method argument 內使用any() (mockito 是用any) >> 第二點是發生在使用mockito 時,`http.get(any)` 沒辦法使用,所以只好換了mocktail 的`http.get(any())`。   這裡的例子是把會使用api 的連線套件,抽換成mock class,並在`when().thenAnswer((){})` 時回傳假資料。   而為了要植入mock 的class,我把`http.client` 暴露出來,也將原先`http.get()` 和`http.post()` 換成了`client.get()`、`client.post()`。  這個專案的backbone api 是用dio 處理的,所以我找了一個套件叫`http-mock-adapter` ,作用和`when().thenAnswer((){})` 的邏輯類似,當dio 在測試時,會去找對應的mock,並當request url 和target url 一樣的時候,就會回傳`reply()` 的`data` 當作假資料傳出。 :::info 專案是使用protobuf ,輸出的response 是byte 的,但ver. 0.4.2 的`http-mock-adapter` 沒辦法傳送byte response,我自己小改了套件內部的dio 和方法,已經推上作者的repo。 update: PR merge 的0.4.3 後,就可以使用 `fromByte` 方法mock protobuf 的response。 ::: --- ## ✅ 成效和感想  完成所有測試只需要2s,因為將後端的因素排除,在沒有網路因素影響下,可以迅速的測試到自己的function,而且也可以藉由test,更清楚知道所寫的邏輯中,應該產出哪些結果。而若是以TDD 的方式在開發的話,就可以先以預期產生的結果來設計邏輯。 不過要在前後端的設計都不太確定的情況下開發,用TDD 開發還會是個選項嗎?
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up