用 Swift 編寫網路層單元測試

發表於2016-05-30

 

單元測試主要用來檢測某個工作單元的結果是否符合預期,以此保證該工作單元的邏輯正確。上次寫封裝一個 Swift-Style 的網路模組的時候在結尾提了一下單元測試的重要性,評論中有朋友對網路層的單元測試有一些疑惑。我推薦他去看《單元測試的藝術》(這本書讓我對單元測試有了新的認識),但由於該書是以 C# 為例寫的,可能會對 iOS 開發的朋友造成一定的閱讀障礙,所以我還是決定填一下坑,簡單介紹一下用 Swift 進行網路層單元測試的方法。不過由於 Swift 的函式式特性,像《單元測試的藝術》中那樣單純地用 OOP 思維編寫測試可能會有些麻煩,本文臨近結尾部分寫了一點自己用過的使用“偽裝函式”進行測試的方法,可能大家以前沒見過,我自己也是突然想到的,歡迎提出各種意見。

網路層的單元測試之所以讓人感覺難以下手,原因主要有兩點:

  • 網路是個不穩定的外部依賴。
  • 網路操作一般會涉及非同步過程,而非同步過程難以測試。

要直接測試網路和非同步呼叫,可以使用XCTest提供的expectationWithDescription+waitForExpectationsWithTimeout,舉個例子:

測試方法按 test方法名_測試場景_期望結果 的格式命名。首先在非同步回撥外面呼叫expectationWithDescription方法得到一個expectation,這個方法接受一個字串,用來描述本次測試,我傳了個空串,因為我們的測試方法名已經足夠清晰了。然後在回撥中呼叫expectation.fulfill()表明滿足測試條件,接下來就可以進行斷言。最後別忘了在回撥外面加上waitForExpectationsWithTimeout(timeout, handler: nil),如果時間超過timeout回撥還沒有執行,就會測試失敗,hander會在超時後呼叫,可以寫一些清空狀態和還原現場的操作,以免影響之後的測試,譬如task?.cancel()。但是我這邊什麼都沒做,因為優秀的單元測試之間本來就不應該互相有影響。

上面的測試非常簡單吧,但是按《單元測試的藝術》一書中的觀點,這樣的測試已經不能算是單元測試,而是步入整合測試的範疇了:

整合測試是對一個工作單元進行的測試,這個測試對被測試的工作單元沒有完全的控制,並使用該單元的一個或多個真實的依賴物,例如時間、網路、資料庫、執行緒或隨機數產生器等。

上述這個測試非常不穩定,它依賴於真實的網路狀況,我們可能因為網路不佳測試失敗,而不是因為我們的程式碼本身有邏輯錯誤,而且這個測試有可能非常慢,慢到你不願意每次一修改程式碼就去跑一遍測試,這樣的單元測試就有可能形同虛設。

整合測試當然也非常重要,但一般開發人員也就寫寫單元測試。其實 Alamofire 就有采用我上面說的方法進行測試,所以如果你的網路層像我一樣是以 Alamofire 為基礎構建的,那就表示你不太需要再去寫這樣的測試了,你只要保證跟 Alamofire 無關的那些程式碼本身邏輯正確,以及正確呼叫了 Alamofire 即可。

譬如針對我的這個方法:

我一般會去測試它的返回值是否符合預期:

這兩個測試基本可以保證檢查 URL 是否合法的邏輯和呼叫 Alamofire 的邏輯正確。

由於該方法中使用了parseResult方法,當然我也要測試這個方法的正確性:

這個測試也是測試返回值,測試了幾種可能發生的情況,基本可以保證parseResult方法的正確性。

工作單元可能有三種最終結果:返回值、改變系統狀態和呼叫第三方物件。相應的單元測試一般可以分為三類:基於返回值的測試、基於狀態的測試和互動測試。我上面幾個測試都是在測試返回值,這種測試最簡單直接也最好維護。要測試狀態的改變一般需要先測試初始狀態,然後呼叫改變狀態的方法,再測試改變後的狀態。而互動測試可能就需要用到 fake (偽物件),fake 分為 stub (存根)和 mock (模擬物件)兩種。stub 和 mock 很類似,它們最大的區別是,你會對 mock 進行斷言,但不會對 stub 進行斷言。換句話說,一旦你對一個 fake 進行斷言了,它就是個 mock,否則就是個 stub。

由於 Swift 的反射非常弱雞,似乎並沒有什麼特別好用的 mock 框架,所以一般來說可以用面向協議的思想來減少物件間的耦合,然後手動構建一個 fake 用於測試,當然這需要一些依賴注入技術的配合。又因為 Alamofire 對外暴露的最常用函式request是個全域性函式,而它又會返回一個Request物件,我們要在該物件上呼叫responseJSON方法,這樣一來光用偽物件似乎不足以滿足需求。

Swift 畢竟是一門對 FP 支援度很高的語言,所以工作單元還可能有第四種最終結果——呼叫第三方函式(這個說法好像怪怪的,領會精神啊哈哈)。那相對應的,我們當然可以使用一個 fake function(偽函式,同樣領會精神即可……)來配合測試。依舊以我的 NetworkManager 為例,稍加改造,方便在測試時注入偽函式和偽物件:

我宣告瞭一個新的型別NetworkRequest,它其實是個函式,簽名跟 Alamofire 中的全域性函式request一致。使用者使用時只需呼叫defaultManager即可,而測試時我們可以手動構建一個符合NetworkRequest簽名的函式通過初始化方法注入到NetworkManager中。我還宣告瞭一個Responsable的協議,然後用extension 顯式宣告 Alamofire 中的Request遵守該協議,這個協議可以讓我們在測試時構建一個代替Request的 fake 物件。

好了,萬事俱備,開始寫測試用例:

我覺得這是非常具有 Swift 風格的單元測試,不知道別人有沒有用過。

相關文章