在讓我們在專案中進行一些程式碼相關的測試吧!
我先自首,我很少在專案上做一些測試相關的程式碼(跪)。但是某些時候有些需要計算或複雜的拼湊時候,會開一個 Unit Test 來跑看看結果就是了。而我想研究這點原因蠻有趣的,讓我娓娓道來。
在隕石開發期間的某個專案中,我們合作的後端工程團隊非常的不穩定,而不穩定的程度大概就是什麼都開的出來,key 跟 type 也是各種亂,而最常見的狀況可能就是打不到。
而我看到我們 iOS 的同事寫了一份 Unit Test 的檔案用來測試後端的每隻 API,測試每個 API 是否打得到結果,並且簡單也測試一些回傳的內容是否正常,像是某個 Object 中
array
是不是空的,透過 Unit Test 前面的 ✅ 或 ❌ 來查看測試是否正確。雖然很多工具可以做到相同的事情甚至更好,但是我覺得這種方式蠻有趣的,就想來看一下測試。
那麼這次文章的主題就非常明確了,就是 ——「Unit Test」也就是單元測試,這篇文章會帶大家了解 Xcode 中的單元測試,並且透過單元測試簡單的測試 APP 的功能以及測試 API 請求相關的異步測試。
當你在找尋 Unit Test 相關的技術文章時,你可能會看到 FIRST 的原則,而這個原則就是我們在單元測試時應該要實現的標準:
遵循這些原則將使你的測試清晰且有用,而不會是你開發中的阻礙。
本篇文章會參考 raywenderlich 上的 iOS Unit Testing and UI Testing Tutorial 這邊測試文章,有興趣的讀者也可以看看。
首先我們需要有一個應用程序讓我們測試,這邊我們簡單寫了一個撲滿的 APP,你可以透過 Moneybox
中的方法來存入金額跟取出金額,而當我們每次在取出金額時,同時也會傳出一個 Bool
值判斷是否取出成功,如果餘額小於取出金額就會傳出 false
。以下是簡單的 APP Demo:
接下來我們在左側視窗中找到 Test Navigation,點選下方的 + 號並選擇 New Unit Test Target...:
通常這個 Test 的名稱應該會是 專案名稱 + Tests
,讓我們看看這個測試的檔案長什麼樣子吧:
首先你可以看到這個檔案前面有許多菱形 ? 的方塊,這個菱形方塊可以用來看到我們的測試結果,而在你可以透過三種方式來執行測試:
1.點選工具列 Product 中的 Test 或是快捷鍵 Command + U:
2.點擊 Test Navigation 中的 Unit 的播放鍵,可單一測試:
3.點選菱形方塊按鈕,可單一測試:
接著你可以試著用上述任何一種方式運行一次測試,看看畫面結果:
因為目前我們測試沒有寫任何東西,所以測試理當都會成功,這時你可以看到我們的菱形方塊出現了,綠色勾勾,也就表示著測試成功。如果有錯誤的話,會出現紅色叉叉:
你可以發現到有一個 measure
區塊很特別,並且編輯器上會有一個灰色的提醒,而這個區塊就是用來放置你想要測量時間的程式碼,並且我們可以點選它查看性能結果:
但這次教學我們不會提及這個,所以我們可以把 testExample
和 testPerformanceExample
兩個函數移除。
首先我們 import
我們的 MonyBox
專案(這邊專案少打一個 e QQ),屏且使用 @testable
屬性標示,這使單元測試能夠訪問 internal
的類型及函數。
接著我們在 MonyboxTests
中新增一個變數 sut
類型為 Moneybox!
,讓我們所有測試都能夠訪問它。而命名為 sut
的原因是表示它為我們的測試目標(System Under Test):
而我們剛剛測試中還有留下兩個函數,分別是 setup
和 tearDown
。首先我們會在 setup
中為測試運行前準備初始狀態,並在測試完成後執行清理,所以我們會在這邊創建 sut
實例。而 tearDown
就是在測試案例結束後提供執行清除的機會,我們會在這邊將 sut
實例給釋放。
接著讓我們編寫第一個測試吧!首先先來測試 Moneybox
在操作一段存入、取出的操作流程後,其 Moneybox
的餘額是否與我們所希望的結果相同:
這邊我們使用 XCTAssertEqual
來判斷 sut.balance
是否與我們預期的 150
相同,如果不同則會測試失敗。這邊你有許多種判斷測試成功與否的依據,你可以根據你的情境選擇不同的判別方式。像是這邊我們判斷是否能夠取出金額,如果餘額不足就會失敗:
特別的是你可以在這個 XCAssert
函數中加入一個 message
,而這個訊息會在你測試失敗的時候顯示出結果,這邊我們示範一個失敗的測試,我們將我們預期的值改為 100
:
然後你也可以在左邊視窗中的 BreakPoint Navigtion 中添加: Test Failure,讓我們測試失敗時停止運作,並且停留在該行。
當發生 Test Failure 時,我們也能夠在下方 console 清楚看見 sut
中的內容來方便我們處理錯誤:
當然我們也可以來測試一些 API 相關的異步操作,這邊我們簡單創建我們的 sut
測試目標,類型為 URLSession
,並且設置其 setup
與 tearDown
:
接著我們新增一個異步測試如下:
這邊讓我們解釋一些特別的地方。第一個是 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 秒就得知錯誤:
那麼這次 iOS 單元測試的教學就到這邊結束了,希望大家學會了怎麼樣在專案中進行一些簡單的測試,以及進行一些異步操作的測試,並且學會如何快速得出結果。而我目前也只會這幾種簡單的測試方法,如果你有什麼在 iOS 上測試的心得與我分享,也歡迎與我交流喔。
如果程式架構用MVC又想寫測試的話推薦參考Test-Driven iOS Development with Swift 4 裡面有比較實際的例子
珍惜生命遠離MVC
感謝大神指點,我會慢慢抽離 MVC 的 QQ