iOS开发

iOS 单元测试简单入门

2017-10-23  本文已影响178人  Lxyang

摘要

为了减少线上bug概率,提高交付质量,目前我们已经加入了质检以及代码互看环节,但是这只对业务逻辑以及代码质量方面的检查,如果还想更加了提高交付质量,那么单元测试也是少不了的。于是,接来了一个月,主要任务还是把单元测试添加到项目中。

单元测试对于我目前来说主要测试两个方面

本文中,主要讲述了Qiuck框架的单元测试编写以及Moya框架自带接口测试功能的使用。

一、单元测试

首先,为了选择是原生的框架还是网络上现有的轮子,在网上与查阅了大量资料,对比后,排除了使用原生的,因为有一些第三方良好的对XCTest进行了一层封装。目前比较流行的开发测试框架中,在github上搜索:BDD(行为驱动开发)或TDD(测试驱动开发),因为我们单元测试关心的是行为,于是搜索倾向BDD,发现star最多的是wikispecta,但是这两个都是oc,由于我们项目目前使用的纯swift开发,后来,找到了另两个,分别为:sleipnirQuick,对比一看,后者的star高达6000多,不难选择,我们使用Quick进行尝试。
我们写单元测试代码时,需要弄清楚哪些地方要写测试,哪些不要,这里就稍带一下:

1.1 Quick介绍

Quick is a behavior-driven development framework for Swift and Objective-C. Inspired by RSpec, Specta, and Ginkgo.
Quick的中文介绍

1.1 使用方法
//  Nimble常用判言
 expect(1 + 1).to(equal(2))
 expect(1.2).to(beCloseTo(1.1, within: 0.1))
 expect(3) > 2
 expect("seahorse").to(contain("sea"))
 expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi"))
 expect(ocean.isClean).toEventually(beTruthy())
1.2 开始编写测试代码
// 这会把所有 public 和 internal (默认访问修饰符) 修饰符暴露给测试代码。但 private 修饰符仍旧保持私有。
@testable import MyDemo
// describe描述需要测试的对象内容,就是Given..when..then中的given
describe("测试我的控制器") {
    
    // context描述测试上下文,也就是这个测试在when来进行,一个describe可以包含多个context来描述类在不同情景下的行为
    context("测试客户经理", {
        
        beforeEach({
            
        })
    })
    
    context("测试团队经理", {
        print("打印---团队经理")
    })
    
    context("测试点击事件", {
        print("打印---点击事件")
    })
}

// MARK: 暂时禁用
/// 方法前面添加一个 x  表示禁用些测试,方法名会打印出来  但里面是不会执行的。
xdescribe("its click") {
    // ...none of the code in this closure will be run.
    
    it("禁的方法", closure: {
        print("禁止的方法,是否打印了出来")
    })
}

// MARK: 临时运行测试用例中的一个子集(在it context describe前面添加f即可)
// 它可以使得我们更加专注一个或几个例子;运行一个或两个例子比全部运行更快。使用fit函数你可以只运行一个或者两个
fit("is loud") {
    print("是不是就只打印了这一个方法呢~~~~fit")
}
import Quick
import Nimble
import RxSwift
import Moya

@testable import MyDemo

class XYJLoginTest: QuickSpec {
    
    override func spec() {
        var loginvm: LoginViewModel?
        var parameters = [String: Any]()
        var disposeBag: DisposeBag?
        
        var phoneNumbers: [String]?
        var pwds: [String]?
        
        describe("测试登录模块") {
            
            // 测试之前
            beforeEach {
                loginvm = LoginViewModel()
                disposeBag = DisposeBag()
                phoneNumbers = ["188****0393","158****1915"]
                pwds = ["111123","112123"]
            }
            // 测试完之后
            afterEach {
                loginvm = nil
                parameters.removeAll()
                disposeBag = nil
                
                phoneNumbers = nil
                pwds = nil
            }
            
            it("验证登录表单", closure: {
                for (i, phoneNumber) in phoneNumbers!.enumerated() {
                    let formResult = loginvm?.validateForm(phoneNumber, pwds![i])
                    
                    expect(formResult).notTo(beNil())
                    switch formResult! {
                    case .InValid(let msg):
                        fail(msg)
                    case .Valid(let dic):
                        expect(dic).notTo(beNil())
                    }
                }
                
            })
            
            it("登录请求", closure: {
                for (i, phoneNumber) in phoneNumbers!.enumerated() {
                    print("手机号码",phoneNumber,"密码",pwds![i])
                    parameters["mobile"] = phoneNumber
                    parameters["login_pwd"] = pwds![i].md5()
                    
                    loginvm?.login(parameters).debug().subscribe(onNext: { (result) in
                        expect(result.data).notTo(beNil())
                        self.notify() // 取消暂停等待,下面会单独说明此方法用处
                        
                    }, onError: { (error) in
                        expect(self.moyaError(error: error as! MoyaError)).to(beTrue())
                        
                        self.notify() // 取消暂停等待,下面会单独说明此方法用处
                    }).disposed(by: disposeBag!)
                    self.wait() // 等待,直到接口有数据返回
                }
            })
        }
        
    }
}
// MARK: 等待
extension QuickSpec {
    override func wait() {
        repeat {
            expectation(forNotification: NSNotification.Name(rawValue: "QuickSpecTest").rawValue, object: nil, handler: nil)
            waitForExpectations(timeout: 30, handler: nil)
        } while(false)
    }
}
// MARK: 继续执行
extension QuickSpec {
    override func notify() {
        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "QuickSpecTest"), object: nil)
    }
}

二、Moya网络框架,接口测试代码编写

2.1 使用sampleData模拟接口返回数据。
/// The protocol used to define the specifications necessary for a `MoyaProvider`.
public protocol TargetType {

    /// The target's base `URL`.
    var baseURL: URL { get }

    /// The path to be appended to `baseURL` to form the full `URL`.
    var path: String { get }

    /// The HTTP method used in the request.
    var method: Moya.Method { get }

    /// The parameters to be incoded in the request.
    var parameters: [String: Any]? { get }

    /// The method used for parameter encoding.
    var parameterEncoding: ParameterEncoding { get }

    /// Provides stub data for use in testing.
    var sampleData: Data { get }

    /// The type of HTTP task to be performed.
    var task: Task { get }

    /// Whether or not to perform Alamofire validation. Defaults to `false`.
    var validate: Bool { get }
}

其中,baseUrl为请求基址,path为剩下部分的请求路径。method:请求方式,parameters请求参数,parameterEncoding为请求参数的编码方式,sampleData为自己模拟返回的二进字,本接下来主要讲这个如何在单元测试中使用。

    override public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
                         requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
                         stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
                         manager: Manager = RxMoyaProvider<Target>.defaultAlamofireManager(),
                         plugins: [PluginType] = [],
                         trackInflights: Bool = false) {
        super.init(endpointClosure: endpointClosure, requestClosure: requestClosure, stubClosure: stubClosure, manager: manager, plugins: plugins, trackInflights: trackInflights)
    }
public enum StubBehavior {
    case Never          //不使用Stub返回数据,即使用网络请求的真实数据
    case Immediate      //立即使用Stub返回数据  立即使用sampleData中的数据
    case Delayed(seconds: NSTimeInterval)  //一段时间间隔后使用Stub返回的数据 一段时间后使用sampleData中的数据
}

于是,我们可以定义一个全局变量isSampleTest来控制这个参数的值即可实现请求是用模拟的数据还是真实网络请求的数据。

    /// 单元测试代码
    let stubClosure: (_ type: T) -> Moya.StubBehavior  = { type1 in
        if isSampleTest {
            return StubBehavior.immediate
        } else {
            return StubBehavior.never
        }
    }

RxMoyaProvider请求写法如下:其中,stubClosure参数,就用上面所写的闭包即可。

public class XYJMoyaHttp<T:TargetType> {

    func sendRequest() -> RxMoyaProvider<T> {

        return RxMoyaProvider<T>.init(endpointClosure: endpointClosure ,requestClosure: requestClosure,stubClosure: stubClosure,plugins: [NetworkLoggerPlugin.init(verbose: true,responseDataFormatter: {JSONResponseDataFormatter($0)}),spinerPlugin,XYJMoyaResponseNetPlugin()])
    }
}

路由中(遵循TargetType协议的类),我们在sampleData中,写好自己想要返回的json,示例如下

import Foundation
import Moya

// MARK: 消息路由
public enum XYJMessageRouter {
    // 获取个人消息 msg_type=1
    case getPersonMsg(parameters: [String : Any])
    // 获取公告消息 msg_type=2
    case getNotice(parameters: [String : Any])
}

extension XYJMessageRouter: TargetType, XYJTargetType {
    
    public var baseURL: URL { return URL(string: baseHostString)! }
    
    public var path: String {
        switch self {
            case .getPersonMsg:
                return "yd/app/user/getNoticeDetail"
            case .getNotice:
                return "yd/app/user/getNoticeDetail"
        }
    }
    
    public var method: Moya.Method {
        
        switch self {
            case .getPersonMsg:
                return .post
            case .getNotice:
                return .post
        }
        
    }
    
    /// The parameters to be incoded in the request.
    public var parameters: [String: Any]? {
        switch self {
        case .getPersonMsg(parameters: let dic):
            return dic
        case .getNotice(parameters: let dic):
            return dic
        }
    }
    
    public var parameterEncoding: ParameterEncoding {
        return JSONEncoding.default
    }
    
    /// The type of HTTP task to be performed.
    public var task: Task {
        return .request
    }
    
    /// Whether or not to perform Alamofire validation. Defaults to `false`.
    public var validate: Bool {
        return false
    }
    
    /// Provides stub data for use in testing.
    public var sampleData: Data {
        switch self {
        case .getPersonMsg:
            return "{\"message\":\"成功\",\"data\":{\"result\":[{\"msg_title\":\"淘宝发布了\",\"msg_detail\":\"淘宝真的发布了\",\"msg_time\":\"2017-10-28 11:22:17\",\"msg_read\":false,\"msg_id\":\"22\",\"url\":\"https://www.taobao.com\"},{\"msg_title\":\"淘宝发布了\",\"msg_detail\":\"淘宝真的发布了淘宝真的发布了淘宝真的发布了淘宝真的发布了\",\"msg_time\":\"2017-10-28 11:22:17\",\"msg_read\":false,\"msg_id\":\"22\",\"url\":\"https://www.taobao.com\"}],\"pages\":\"3\",\"page_index\":\"1\",\"page_size\":\"10\",\"total\":\"24\",\"has_next_page\":true},\"code\":\"0\"}".data(using: String.Encoding.utf8)!
        case .getNotice:
            return "{\"message\":\"成功\",\"data\":{\"result\":[{\"msg_title\":\"淘宝发布了\",\"msg_detail\":\"淘宝真的发布了\",\"msg_time\":\"2017-10-28 11:22:17\",\"msg_read\":false,\"msg_id\":\"22\",\"url\":\"https://www.taobao.com\"},{\"msg_title\":\"淘宝发布了\",\"msg_detail\":\"淘宝真的发布了淘宝真的发布了淘宝真的发布了淘宝真的发布了淘宝真的发布了淘宝真的发布了淘宝真的发布了\",\"msg_time\":\"2017-10-28 11:22:17\",\"msg_read\":false,\"msg_id\":\"22\",\"url\":\"https://www.taobao.com\"}],\"pages\":\"3\",\"page_index\":\"1\",\"page_size\":\"10\",\"total\":\"24\",\"has_next_page\":true},\"code\":\"0\"}".data(using: String.Encoding.utf8)!
        }
    }

上文中,samleData中的json数据,按照接口文档编写即可。

上一篇下一篇

猜你喜欢

热点阅读