iOS - MVVM/MVP 下的單元測試總結(jié)

背景

不得不說對于移動端, 進行單元測試的不像后臺那樣普遍.. 很多小公司的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)錯誤的功能.. 也是沒有意義的. .或者說, 除了需要單元測試的地方剩下的就是不需要做的啦~哈哈哈

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末歧胁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瞧预,更是在濱河造成了極大的恐慌提澎,老刑警劉巖透典,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灸撰,死亡現(xiàn)場離奇詭異,居然都是意外死亡烧栋,警方通過查閱死者的電腦和手機巷挥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門桩卵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人倍宾,你說我怎么就攤上這事雏节。” “怎么了高职?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵钩乍,是天一觀的道長。 經(jīng)常有香客問我怔锌,道長寥粹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任埃元,我火速辦了婚禮涝涤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘岛杀。我一直安慰自己阔拳,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布类嗤。 她就那樣靜靜地躺著糊肠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪遗锣。 梳的紋絲不亂的頭發(fā)上货裹,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音精偿,去河邊找鬼弧圆。 笑死赋兵,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的搔预。 我是一名探鬼主播毡惜,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼斯撮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扶叉,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤勿锅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后枣氧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溢十,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年达吞,在試婚紗的時候發(fā)現(xiàn)自己被綠了张弛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡酪劫,死狀恐怖吞鸭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情覆糟,我是刑警寧澤刻剥,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站滩字,受9級特大地震影響造虏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜麦箍,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一漓藕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挟裂,春花似錦享钞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至交排,卻和暖如春划滋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背埃篓。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工处坪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓同窘,卻偏偏與公主長得像玄帕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子想邦,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,382評論 8 265
  • 1裤纹、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,980評論 3 119
  • 尊敬的師父您好!總部基地的各位老師丧没,全國各地的校長們鹰椒,大健康的兄弟姐妹們,大家晚上好呕童! 我是陜西中心校的曹福強漆际,在...
    張蓉萍閱讀 251評論 0 0
  • 新工作工作內(nèi)容幾乎半個人事,這幾天做新入職員工的背調(diào)夺饲,頗有感觸奸汇。 有月薪10k到50k再回到10k,工作經(jīng)歷起起伏...
    Demooo閱讀 171評論 0 0
  • 原材料弓|用(Materials) More than one thousand of the victims r...
    小Q_先生閱讀 420評論 0 0