美文网首页SwiftiOS 的那些事儿
iOS - MVVM/MVP 下的单元测试总结

iOS - MVVM/MVP 下的单元测试总结

作者: Zafir_zzf | 来源:发表于2018-09-29 15:33 被阅读32次

    背景

    不得不说对于移动端, 进行单元测试的不像后台那样普遍.. 很多小公司的iOS开发可能一听到"测试"两个字都会觉得是测试人员做的事情. 也不会要求对项目进行单元测试.. 我总结了一下原因主要是

    • 苹果的Cocoa框架所推崇的MVC模式因为视图与逻辑的紧耦合无法进行分离进行单元测试.而大部分公司用的也是MVC.
    • 单元测试耗时费力, 因为项目进度公司不做要求, 员工也不主动测.
    • 没有意识到单元测试在移动端的意义, 认为集成测试即可达到测试目的..

    我也是在一个小公司, 得益于Leader的目光长远, 整个团队做的项目前后台都是要进行单元测试的. 所以项目也是MVVM的模式..

    意义

    • 验证某一很小的单元功能的正确性.
    • 可以确保重构的时候, 不怕影响到业务逻辑.因为通过运行单元测试可以迅速定位出遗漏的功能.
    • 通过某一功能能否进行单元测试反映代码的解耦程度, 那一定是可以维护的没有违背单一职责的代码. 也就是说因为要进行单元测试开发者会更加注意代码质量, 避免写出紧耦合的代码
    • 每次迭代新功能后运行单元测试可以排除可能存在的相关联的bug..
    • 增强开发者对自己代码的信心.

    哪些地方需要做单元测试

    以下示例代码均在MVVM(没有采用Binding-Data), Swift语言下环境下

    View-Action

    关于视图的事件, 也就是用户的操作事件. 可能是点击了某一个按钮, 输入了某些字符. 操作事件可能会修改数据Model, 可能会变更视图状态, 可能两者同时都会. .

    在测试中通过主动调用View-Action的方法, Mock假数据来观察model前后的变化来验证在操作时间之后我们是否对model进行了符合功能的修改. 通过观察有没有调用相应View的更新视图方法来验证即时刷新视图.(如果用了数据绑定后一步可以省略, 因为视图肯定会跟随数据变化了.)

    示例: 比如现在有一个展示某一商品销量的列表, 用户点击排序按钮后可以对销量进行正序或倒序重新排列. 那么点击按钮这个操作就是View-Action, 我们需要测试的是用户点击按钮之后数据有没有按照预期进行排列, 以及 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)
    }
    

    第一步先生成一组销量乱套的数据源并且赋值给vm. 接着vm调用sort方法, 排序规则是销量正序. 调用完之后通过数据源中的第一个以及最后一个数据来验证排序逻辑是否正确.
    前提是你的view-action事件最终会调用vm中的这个方法, 排序这个函数对不同的排序规则进行了抽取. 接下来销量倒序, 价格正序同理通过调用方法, 即可通过数据源验证.

    接下来还要刷新视图, 通过修改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是通过接口进行通信的, 通过创建一个conform这个接口view的对象并重新赋值给VM, 可以模拟出View是否调用了相关方法.

    Model-Action

    数据的变化, 通常是网络请求数据, 读取缓存, Socket推送来的数据或者计时器对数据的修改. 这种非用户操作的数据变更之后, 我们要测试View有没有做对应的刷新处理.

    示例 请求一个列表数据, 但是当列表数据超出20的时候只筛选前20个.

    在单元测试中为了制造用例, 网络请求的方法都要能进行Fake出假数据并提供给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, "最新成交数量不超过20")
    

    MarketRequest是一个请求类, 它作为VM的属性, 在loadNewestDealOrders方法中会进行请求. 我们通过创建一个Request的子类FakeRequest重新赋值给VM, 当vm在调用load方法时就会调用这个FakeRequest中override了的函数.

    vm.loadNewestDealOrders()方法具体实现是什么我们不知道. 我们只要保证在vm调用了这个方法之后数据源的个数是20个就可以了.这就证明新数据已经进行了赋值, 并且筛选到了20个.因为我们假数据中是31个. 验证view是否更新的测试同上

    数据变形

    请求完数据到赋值给view刷新的过程当中我们大部分时候还会对数据进行一些处理变形为view直接需要的样子. 同时也会根据不同的业务逻辑做不同处理. 个人认为这些测试在某些项目中是非常有必要的. 比如金融类项目, 多大的数字保留几位小数, 百分比显示的时候是否进行了四舍五入等.

    示例: 列表中某一商品的成交量要取整数, 并且每三位用逗号隔开, 我们要验证数据展示时是否进行了取整并加逗号..

    如果数据变形都是在cell中执行的. 那我们就要创建一个cell, 调用封装好的赋值方法, 接下来通过获取Label的Text属性来验证正确性了. 先不说这种方式比较麻烦, cell中视图如果更换名称或者好几个cell都要用到这个变形那我们可要测好几个cell中的label. 数据变形的代码也应该放到vm中进行. 用于展示cell数据的VM我取名为cellVM, cellVM中有提供给cell直接用来展示的属性.

    symbol.vol = 123456789.32
    cellVM = MarketListCellVM(symbol: symbol)
    let volume2Correct = cellVM.volumeText == volumeFormatePrefix + "123,456,789"
    XCTAssertTrue(volume2Correct)
    

    这种变形输入的情况越多越准确, 所有的极限,边界情况都可以通过输入测试到.

    具有输入输出的工具方法

    这个要取决于你封装的方法了, 有些封装的方法因为里面冗杂了特别多的判断而导致不够独立, 那么对应功能方法还是无法测试. 所以尽量把函数方法抽为独立的最小单元是非常有必要的.

    示例: 测试一个比较日期的工具方法

     /// 比较两个日期之间超过了多少天否
        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)
        }
    

    这个是它的实现, 具有输入参数以及返回值. 测起来相当简单

    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天前与现在超过4天")
     }
    

    像这种有输入输出的工具类的测试应该是最适宜单元测试也是最能体现意义的了
    它直接验证了某一函数功能的正确性.. 其它的几种可能会让你觉得"不是那么有意义", 并且想要测完整了, 代码的解耦也是一种挑战. 但是当项目复杂起来, 一个功能模块的view-action和model-action多起来之后没有单元测试会让你觉得战战兢兢, 新添功能和重构一点代码可能会牵扯到其它的东西是很有可能的.

    哪些地方不需要做单元测试

    单元测试的意义虽然有很多, 在view与逻辑解耦之后有不少函数功能是可以测试的, 但是并不是说测的代码越多越好. 有时候耗时耗力写了不少用例, 但其实验证的只是一个系统函数或者是完全不会出现错误的功能.. 也是没有意义的. .或者说, 除了需要单元测试的地方剩下的就是不需要做的啦~哈哈哈

    相关文章

      网友评论

        本文标题:iOS - MVVM/MVP 下的单元测试总结

        本文链接:https://www.haomeiwen.com/subject/rhnvoftx.html