iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 29
1
Mobile Development

諸神黃昏下的 iOS 工程師系列 第 29

D29 - 讓我們來測試看看你的 APP 功能是否正常吧!

在讓我們在專案中進行一些程式碼相關的測試吧!

? 隕石小故事

我先自首,我很少在專案上做一些測試相關的程式碼(跪)。但是某些時候有些需要計算或複雜的拼湊時候,會開一個 Unit Test 來跑看看結果就是了。而我想研究這點原因蠻有趣的,讓我娓娓道來。

在隕石開發期間的某個專案中,我們合作的後端工程團隊非常的不穩定,而不穩定的程度大概就是什麼都開的出來,key 跟 type 也是各種亂,而最常見的狀況可能就是打不到。

而我看到我們 iOS 的同事寫了一份 Unit Test 的檔案用來測試後端的每隻 API,測試每個 API 是否打得到結果,並且簡單也測試一些回傳的內容是否正常,像是某個 Object 中 array 是不是空的,透過 Unit Test 前面的 ✅ 或 ❌ 來查看測試是否正確。雖然很多工具可以做到相同的事情甚至更好,但是我覺得這種方式蠻有趣的,就想來看一下測試。

Overview

那麼這次文章的主題就非常明確了,就是 ——「Unit Test」也就是單元測試,這篇文章會帶大家了解 Xcode 中的單元測試,並且透過單元測試簡單的測試 APP 的功能以及測試 API 請求相關的異步測試。


單元測試中的 FIRST

當你在找尋 Unit Test 相關的技術文章時,你可能會看到 FIRST 的原則,而這個原則就是我們在單元測試時應該要實現的標準:

  • Fast(快速):沒錯,就是要快。測試應該要能夠快速的進行。
  • Independent/Isolated(獨立/抽離):測試不應該共享狀態或是互相依賴,如果互相有依賴那麼複雜度就會變高,就難以從測試中找出問題。
  • Repeatable(可重複性):在正常環境下,每次運行測試時,都應該可以得出相同的結果。除了一些外部因素及意外狀況之外(網路速度過慢,無法連線等等)。
  • Self-validating(自我驗證):測試應該要完全自動化,並且透過「成功」和「失敗」
  • Timely(及時性):理想情況下,應該在編寫要測試的生產代碼之前編寫測試,也就是 TDD(測試驅動開發)。

遵循這些原則將使你的測試清晰且有用,而不會是你開發中的阻礙。


實作

本篇文章會參考 raywenderlich 上的 iOS Unit Testing and UI Testing Tutorial 這邊測試文章,有興趣的讀者也可以看看。

|事前準備

首先我們需要有一個應用程序讓我們測試,這邊我們簡單寫了一個撲滿的 APP,你可以透過 Moneybox 中的方法來存入金額跟取出金額,而當我們每次在取出金額時,同時也會傳出一個 Bool 值判斷是否取出成功,如果餘額小於取出金額就會傳出 false。以下是簡單的 APP Demo:

|快速介紹 Xcode 的 Unit test

接下來我們在左側視窗中找到 Test Navigation,點選下方的 + 號並選擇 New Unit Test Target...:

通常這個 Test 的名稱應該會是 專案名稱 + Tests,讓我們看看這個測試的檔案長什麼樣子吧:

首先你可以看到這個檔案前面有許多菱形 ? 的方塊,這個菱形方塊可以用來看到我們的測試結果,而在你可以透過三種方式來執行測試:

1.點選工具列 Product 中的 Test 或是快捷鍵 Command + U:

2.點擊 Test Navigation 中的 Unit 的播放鍵,可單一測試:

3.點選菱形方塊按鈕,可單一測試:

接著你可以試著用上述任何一種方式運行一次測試,看看畫面結果:

因為目前我們測試沒有寫任何東西,所以測試理當都會成功,這時你可以看到我們的菱形方塊出現了,綠色勾勾,也就表示著測試成功。如果有錯誤的話,會出現紅色叉叉:

你可以發現到有一個 measure 區塊很特別,並且編輯器上會有一個灰色的提醒,而這個區塊就是用來放置你想要測量時間的程式碼,並且我們可以點選它查看性能結果:

但這次教學我們不會提及這個,所以我們可以把 testExampletestPerformanceExample 兩個函數移除。


|使用 Unit Test 測試

首先我們 import 我們的 MonyBox 專案(這邊專案少打一個 e QQ),屏且使用 @testable 屬性標示,這使單元測試能夠訪問 internal 的類型及函數。

接著我們在 MonyboxTests 中新增一個變數 sut 類型為 Moneybox!,讓我們所有測試都能夠訪問它。而命名為 sut 的原因是表示它為我們的測試目標(System Under Test):

而我們剛剛測試中還有留下兩個函數,分別是 setuptearDown。首先我們會在 setup 中為測試運行前準備初始狀態,並在測試完成後執行清理,所以我們會在這邊創建 sut 實例。而 tearDown 就是在測試案例結束後提供執行清除的機會,我們會在這邊將 sut 實例給釋放。

接著讓我們編寫第一個測試吧!首先先來測試 Moneybox 在操作一段存入、取出的操作流程後,其 Moneybox 的餘額是否與我們所希望的結果相同:

這邊我們使用 XCTAssertEqual 來判斷 sut.balance 是否與我們預期的 150 相同,如果不同則會測試失敗。這邊你有許多種判斷測試成功與否的依據,你可以根據你的情境選擇不同的判別方式。像是這邊我們判斷是否能夠取出金額,如果餘額不足就會失敗:

特別的是你可以在這個 XCAssert 函數中加入一個 message,而這個訊息會在你測試失敗的時候顯示出結果,這邊我們示範一個失敗的測試,我們將我們預期的值改為 100

然後你也可以在左邊視窗中的 BreakPoint Navigtion 中添加: Test Failure,讓我們測試失敗時停止運作,並且停留在該行。

當發生 Test Failure 時,我們也能夠在下方 console 清楚看見 sut 中的內容來方便我們處理錯誤:


|測試異步操作

當然我們也可以來測試一些 API 相關的異步操作,這邊我們簡單創建我們的 sut 測試目標,類型為 URLSession,並且設置其 setuptearDown

接著我們新增一個異步測試如下:

這邊讓我們解釋一些特別的地方。第一個是 expectation,其 description 表示你預期發生的事情,我們會之後會在異步方法成功執行時調用 fulfill 函數來表示已達成期望。

wait 函數是讓我們用來等待一組期望值,直到超出時間為止。也就是說如果在 timeout 發生時沒有滿足所有期望值,測試將會失敗。


|快速失敗

我參考的這篇文章的作者打了一句很有趣的話,但很中肯:

Failure hurts, but it doesn’t have to take forever.

以上面的 testBookshelfAPIStatusCode 例子來說。假設我們運行都將失敗,但總是需要等待其超出時間(timeout)為止,因為我們都是假設請求始終會成功,所以在我們預期的點執行 fulfill。但是由於請求失敗,往往都需要 timeout 時才看出結果。

我們可以透過更改假設來改進這點,並使測試更快得到失敗的結果。不需要等待請求成功,而是要等到異步方法的 completion handler 被調用。一旦從服務器接收到滿足預期的 response(無論是正常還是錯誤),就會觸發這個情況。然後,你的測試就可以檢查請求是否成功。

讓我們新增一個 testBookshelfAPICompletes 的測試吧:

而這兩者的關鍵差別在於我們在 testBookshelfAPICompletes 函數調用 completion handler 時就執行 fulfill 來表示成功運行、滿足期望,而這只需要非常短的時間。如果請求失敗,則會在下面的 XCTAssert 中出現錯誤。

讓我們測試一下兩者的區別吧,其 timeout 時間都是 5 秒,並且將其 url 設置為無效的網址。

第一個測試 testBookshelfAPIStatusCode,我們直到其 timeout 才得知錯誤:

而第二個測試 testBookshelfAPICompletes,我們只用了 0.092 秒就得知錯誤:


Summary

那麼這次 iOS 單元測試的教學就到這邊結束了,希望大家學會了怎麼樣在專案中進行一些簡單的測試,以及進行一些異步操作的測試,並且學會如何快速得出結果。而我目前也只會這幾種簡單的測試方法,如果你有什麼在 iOS 上測試的心得與我分享,也歡迎與我交流喔。

Reference


上一篇
D28 - 你的輸入,我看得見
下一篇
D30 - 沒有制度?那就自己創造一個吧!
系列文
諸神黃昏下的 iOS 工程師31

1 則留言

0
陳董粉絲
iT邦新手 5 級 ‧ 2019-10-17 10:16:14

如果程式架構用MVC又想寫測試的話推薦參考Test-Driven iOS Development with Swift 4 裡面有比較實際的例子

珍惜生命遠離MVC

感謝大神指點,我會慢慢抽離 MVC 的 QQ

我要留言

立即登入留言