背景
不得不說對于移動端, 進行單元測試的不像后臺那樣普遍.. 很多小公司的iOS開發(fā)可能一聽到"測試"兩個字都會覺得是測試人員做的事情. 也不會要求對項目進行單元測試.. 我總結(jié)了一下原因主要是
- 蘋果的Cocoa框架所推崇的MVC模式因為視圖與邏輯的緊耦合無法進行分離進行單元測試.而大部分公司用的也是MVC.
- 單元測試耗時費力, 因為項目進度公司不做要求, 員工也不主動測.
- 沒有意識到單元測試在移動端的意義, 認為集成測試即可達到測試目的..
我也是在一個小公司, 得益于Leader的目光長遠, 整個團隊做的項目前后臺都是要進行單元測試的. 所以項目也是MVVM的模式..
意義
- 驗證某一很小的單元功能的正確性.
- 可以確保重構(gòu)的時候, 不怕影響到業(yè)務邏輯.因為通過運行單元測試可以迅速定位出遺漏的功能.
- 通過某一功能能否進行單元測試反映代碼的解耦程度, 那一定是可以維護的沒有違背單一職責的代碼. 也就是說因為要進行單元測試開發(fā)者會更加注意代碼質(zhì)量, 避免寫出緊耦合的代碼
- 每次迭代新功能后運行單元測試可以排除可能存在的相關聯(lián)的bug..
- 增強開發(fā)者對自己代碼的信心.
哪些地方需要做單元測試
以下示例代碼均在MVVM(沒有采用Binding-Data), Swift語言下環(huán)境下
View-Action
關于視圖的事件, 也就是用戶的操作事件. 可能是點擊了某一個按鈕, 輸入了某些字符. 操作事件可能會修改數(shù)據(jù)Model, 可能會變更視圖狀態(tài), 可能兩者同時都會. .
在測試中通過主動調(diào)用View-Action的方法, Mock假數(shù)據(jù)來觀察model前后的變化來驗證在操作時間之后我們是否對model進行了符合功能的修改. 通過觀察有沒有調(diào)用相應View的更新視圖方法來驗證即時刷新視圖.(如果用了數(shù)據(jù)綁定后一步可以省略, 因為視圖肯定會跟隨數(shù)據(jù)變化了.)
示例: 比如現(xiàn)在有一個展示某一商品銷量的列表, 用戶點擊排序按鈕后可以對銷量進行正序或倒序重新排列. 那么點擊按鈕這個操作就是View-Action, 我們需要測試的是用戶點擊按鈕之后數(shù)據(jù)有沒有按照預期進行排列, 以及 View是否進行了刷新.
func testSortMarketDataVolumeUp() {
let randomSymbols = [Symbol].deserialize(from: [["vol": 3],["vol": 8],["vol": 2],["vol": 2.5],["vol": 5]]) as! [Symbol]
vm.market.marketData = randomSymbols
vm.sortMarketData(with: .volume(type: .up))
XCTAssertTrue(vm.market.marketData[0].vol == 2)
XCTAssertTrue(vm.market.marketData.last!.vol == 8)
}
第一步先生成一組銷量亂套的數(shù)據(jù)源并且賦值給vm. 接著vm調(diào)用sort方法, 排序規(guī)則是銷量正序. 調(diào)用完之后通過數(shù)據(jù)源中的第一個以及最后一個數(shù)據(jù)來驗證排序邏輯是否正確.
前提是你的view-action事件最終會調(diào)用vm中的這個方法, 排序這個函數(shù)對不同的排序規(guī)則進行了抽取. 接下來銷量倒序, 價格正序同理通過調(diào)用方法, 即可通過數(shù)據(jù)源驗證.
接下來還要刷新視圖, 通過修改vm的FakeView就可以進行測試了.
class FakeListV: IMarketListView {
var reloadTableEp = XCTestExpectation(description: "刷新列表視圖")
}
override func setUp() {
super.setUp()
vm.view = FakeListV()
}
func testSortMarketDataViewReload() {
vm.handleSortTypeChange(sortType: SortKindType.newest(type: .up))
wait(for: [defaultFakeV.reloadTableEp], timeout: 1)
}
這得益于VM與View是通過接口進行通信的, 通過創(chuàng)建一個conform這個接口view的對象并重新賦值給VM, 可以模擬出View是否調(diào)用了相關方法.
Model-Action
數(shù)據(jù)的變化, 通常是網(wǎng)絡請求數(shù)據(jù), 讀取緩存, Socket推送來的數(shù)據(jù)或者計時器對數(shù)據(jù)的修改. 這種非用戶操作的數(shù)據(jù)變更之后, 我們要測試View有沒有做對應的刷新處理.
示例 請求一個列表數(shù)據(jù), 但是當列表數(shù)據(jù)超出20的時候只篩選前20個.
在單元測試中為了制造用例, 網(wǎng)絡請求的方法都要能進行Fake出假數(shù)據(jù)并提供給VM才行. 所以
class FakeRequest: MarketRequest {
override func getNewestDeal(observeKey: String, id: Id, callback: @escaping ([NewestDeal]) -> Void) {
var result = [NewestDeal]()
var newestDeal = NewestDeal()
newestDeal.id = 1
newestDeal.price = 10
for _ in 0 ..< 31 {
result.append(newestDeal)
}
callback(result)
}
}
vm.request = FakeRequest()
vm.loadNewestDealOrders()
XCTAssertTrue(vm.newestDeals.count == 20, "最新成交數(shù)量不超過20")
MarketRequest是一個請求類, 它作為VM的屬性, 在loadNewestDealOrders方法中會進行請求. 我們通過創(chuàng)建一個Request的子類FakeRequest重新賦值給VM, 當vm在調(diào)用load方法時就會調(diào)用這個FakeRequest中override了的函數(shù).
vm.loadNewestDealOrders()方法具體實現(xiàn)是什么我們不知道. 我們只要保證在vm調(diào)用了這個方法之后數(shù)據(jù)源的個數(shù)是20個就可以了.這就證明新數(shù)據(jù)已經(jīng)進行了賦值, 并且篩選到了20個.因為我們假數(shù)據(jù)中是31個. 驗證view是否更新的測試同上
數(shù)據(jù)變形
請求完數(shù)據(jù)到賦值給view刷新的過程當中我們大部分時候還會對數(shù)據(jù)進行一些處理變形為view直接需要的樣子. 同時也會根據(jù)不同的業(yè)務邏輯做不同處理. 個人認為這些測試在某些項目中是非常有必要的. 比如金融類項目, 多大的數(shù)字保留幾位小數(shù), 百分比顯示的時候是否進行了四舍五入等.
示例: 列表中某一商品的成交量要取整數(shù), 并且每三位用逗號隔開, 我們要驗證數(shù)據(jù)展示時是否進行了取整并加逗號..
如果數(shù)據(jù)變形都是在cell中執(zhí)行的. 那我們就要創(chuàng)建一個cell, 調(diào)用封裝好的賦值方法, 接下來通過獲取Label的Text屬性來驗證正確性了. 先不說這種方式比較麻煩, cell中視圖如果更換名稱或者好幾個cell都要用到這個變形那我們可要測好幾個cell中的label. 數(shù)據(jù)變形的代碼也應該放到vm中進行. 用于展示cell數(shù)據(jù)的VM我取名為cellVM, cellVM中有提供給cell直接用來展示的屬性.
symbol.vol = 123456789.32
cellVM = MarketListCellVM(symbol: symbol)
let volume2Correct = cellVM.volumeText == volumeFormatePrefix + "123,456,789"
XCTAssertTrue(volume2Correct)
這種變形輸入的情況越多越準確, 所有的極限,邊界情況都可以通過輸入測試到.
具有輸入輸出的工具方法
這個要取決于你封裝的方法了, 有些封裝的方法因為里面冗雜了特別多的判斷而導致不夠獨立, 那么對應功能方法還是無法測試. 所以盡量把函數(shù)方法抽為獨立的最小單元是非常有必要的.
示例: 測試一個比較日期的工具方法
/// 比較兩個日期之間超過了多少天否
static func compareTwoDates(left: Date, right: Date, beyond daysNum: Int) -> Bool {
let differ = fabs(right.timeIntervalSince1970 - left.timeIntervalSince1970)
return daysNum.days.to(.seconds) < Int(differ)
}
這個是它的實現(xiàn), 具有輸入?yún)?shù)以及返回值. 測起來相當簡單
func testCompareTwoDates() {
let date1 = 3.days.earlier
let date2 = 5.days.earlier
let result1 = DateTool.compareTwoDates(left: date1, right: date2, beyond: 1)
XCTAssert(result1, "3與5天超過1天")
let date3 = 3.days.earlier
let result2 = DateTool.compareTwoDates(left: date3, right: Date(), beyond: 4)
XCTAssert(!result2, "3天前與現(xiàn)在超過4天")
}
像這種有輸入輸出的工具類的測試應該是最適宜單元測試也是最能體現(xiàn)意義的了
它直接驗證了某一函數(shù)功能的正確性.. 其它的幾種可能會讓你覺得"不是那么有意義", 并且想要測完整了, 代碼的解耦也是一種挑戰(zhàn). 但是當項目復雜起來, 一個功能模塊的view-action和model-action多起來之后沒有單元測試會讓你覺得戰(zhàn)戰(zhàn)兢兢, 新添功能和重構(gòu)一點代碼可能會牽扯到其它的東西是很有可能的.
哪些地方不需要做單元測試
單元測試的意義雖然有很多, 在view與邏輯解耦之后有不少函數(shù)功能是可以測試的, 但是并不是說測的代碼越多越好. 有時候耗時耗力寫了不少用例, 但其實驗證的只是一個系統(tǒng)函數(shù)或者是完全不會出現(xiàn)錯誤的功能.. 也是沒有意義的. .或者說, 除了需要單元測試的地方剩下的就是不需要做的啦~哈哈哈