基于Moya + RxSwift + ReactorKit 框架下的 Unit Tests 探讨

在这个架构下我们主要讨论两个模块的单元测试,一个是网络模块,一个Reactor模块。

1.网络层单元测试

做网络请求测试时,我们希望给定一个测试数据时,就能同步返回这个数据。不需要异步的去服务器上获取。Moya框架提供了一个SampleData来专门用来单元测试使用,就不需要我们去Mock一个网络对象了。

在定义的Moya的Target文件里面设置sampleData属性。

var sampleData: Data {
    switch self {
    case .getAllProducts:
        return "".data(using: .utf8)!
    default:
        return "".data(using: .utf8)!
    }
}

然后设置一下MoyaProvider的stubClosure。StubBehavior是一个枚举值。

  • .never没有Stub,
  • .immediate 同步返回SampleData里面设置的数据,
  • .delayed(seconds:) 延迟时间返回SampleData里面设置的数据

我们这里当然设置成MoyaProvider.immediatelyStub,发起Request之后就会立即返回sampleData里面的数据。

let provider = MoyaProvider(stubClosure: MoyaProvider.immediatelyStub)
provider.rx.request(target)

接下来我们就可以在单元测试文件里面写测试Case了。

为了方便测试我们写了个服务层,例如HomeService就是关于Home界面的网络请求。测试home界面相关网络请求我们只需要测试HomeService这个类就可以了。

比如我们想测试一个404错误请求的Case:

func testToError_notFound() {

    //endpointClosure 自定义成我们想测试404error
    let endpointClosure = { (target: NetworkTarget) -> Endpoint in
        let url = URL(target: target).absoluteString
        return Endpoint(url: url, sampleResponseClosure: {.networkError(NSError(domain: "not fount", code: 404, userInfo: nil))}, method: target.method, task: target.task, httpHeaderFields: target.headers)
    }
    //创建一个立即返回Data的Provider
    let provider = MoyaProvider(endpointClosure: endpointClosure,stubClosure: MoyaProvider.immediatelyStub)
    let netwoking = Network(provider: provider)
    var netError: NetworkError? = nil
    ServiceManager(networking: netwoking)
        .homeService
        .getAllProducts(page: 0)
        .subscribe(onSuccess: { (weatherData) in
            
        }, onError: { (error) in
              //拿到返回的错误信息
            netError = NetworkError(error: error)
        })
        .disposed(by: disposeBag)
    
    //与预期的结果做对比
    XCTAssertEqual(netError, NetworkError.notFound)
}

RxBlocking

Rx提供一个RxBlocking框架,专门用来做单元测试,RxBlocking将阻塞当前线程一直到观察者序列(observable)终止,toBlocking()就是RxBlocking提供的一个方法,它可以把原始的Observable变成一个BlockingObservable。这个BlockingObservable可以阻断当前线程,让我们用它提供的方法等待特定的事件发生。其中常用的三个方法是:

  • toArray()把Observable中发生的所有事件,转换成一个[T]。这个方法只适用于有限序列,我们就可以用数组的形式观察到Observable中的所有值;
  • first(),得到Observable中第一个事件的值;
  • last(),得到Observable中最后一个事件的值;

用toBlocking的方式写一个返回正确Data的Case:

func testData_allProductInfos() {

    //endpointClosure 自定义成我们相反会的正确data
    let endpointClosure = { (target: NetworkTarget) -> Endpoint in
        let url = URL(target: target).absoluteString
        return Endpoint(url: url, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: target.headers)
    }
    let provider = MoyaProvider(endpointClosure: endpointClosure,stubClosure: MoyaProvider.immediatelyStub)
    let netwoking = Network(provider: provider)
    var response:[ProductInfo]?
    do {
        response = try ServiceManager(networking: netwoking)
            .homeService
            .getAllProducts(page: 0)
            .toBlocking()
            .first()
    } catch  {
            
    }
    
    //预期的数据模型
    let data = CommonTools.shareInstance.loadDataFromBundle(ofName: "AllProductInfo", ext: "json")
    let dictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
    let mesageDic = dictionary!["data"] as? [[String: Any]]
    let models = Mapper().mapArray(JSONObject: mesageDic)
    
    //用返回的结果与预期的数据模型做对比
    XCTAssertEqual(response, models)
}

2.对Reactor单元测试

项目中我们把业务逻辑代和数据都放到Reactor中,所以当我们想测试一个模块时,只需要测试它的Reactor。比如想要测试Home界面,我们直接测试HomeReactor。

我们以PeiPeiPay项目首页作为一个例子:

EV0KTH.png

如图所示我们在HomeReactor中会把网络请求的模型数据转换为Cell需要显示的数据:

//cell直接显示的数据模型
struct GoodListCellData: Equatable {
    
    var name: String?
    var imageUrl: String?
    var info: String?
    var salePrice: String?
    var repertory: String?
    var originalPrice: NSAttributedString?
    
    static func == (lhs: GoodListCellData, rhs: GoodListCellData) -> Bool {
        return lhs.name == rhs.name
                && lhs.imageUrl == rhs.imageUrl
                && lhs.info == rhs.info
                && lhs.salePrice == rhs.salePrice
                && lhs.repertory == rhs.repertory
                && lhs.originalPrice == rhs.originalPrice
    }
}

这样的话我们只需要测试GoodListCellData的数据是否正确就能确定cell上显示的数据是否正确了。

首先自定义了一个Json数据

{
    "data" : [{
                    "name" : "黑巧克力",
                    "image_url" : "https://s2.ax1x.com/2019/04/19/Epj1HI.png",
                    "item_code" : "1",
                    "sale_price" : 20.1,
                    "cost_price" : 30.1,
                    "count" : 20,
                    "note" : "好吃又好玩",
                    "category" : "食品",
              }]
}

Tests文件的setUp发起网络请求

override func setUp() {
    super.setUp()
    let provider = MoyaProvider(stubClosure: MoyaProvider.immediatelyStub)
    let networking = Network(provider: provider)
    reactor = HomeViewReactor(serviceManager: ServiceManager(networking: networking))
    reactor.action.onNext(.downRefresh(searchName: ""))
}

然后我们就能写测试Case来验证Reactor里面的GoodListCellData数据和我们预期的json数据是否一致。

//验证商品名
func testAllProductItems_name() {
    
    XCTAssertEqual(reactor.currentState.goodListSectionModel[0].data[0].name, "黑巧克力")
    
}

//验证售价
func testAllProductItems_salePrice() {
    
    XCTAssertEqual(reactor.currentState.goodListSectionModel[0].data[0].salePrice, "售价:20.1")
    
}

//验证原价
func testAllProductItems_originalPrice() {
    
    let original = CommonTools.shareInstance.addlineToLabelText(text: "原价:30.1")
    XCTAssertEqual(reactor.currentState.goodListSectionModel[0].data[0].originalPrice, original)
    
}

在此框架下我们如果对所有Server和Reactor都进行了单元测试,其实已经能覆盖大部分的测试。

Eeam0e.png

EeaQfI.png

使用Xcode可以查看测试覆盖率

编写好测试用例之后,我们来看如何查看这些用例覆盖的代码范围。

  • 选择Test Scheme;
  • 切换到Options tab;
  • 选中Gather coverage for;
  • 切换到some targets;
  • 在下面的Targets列表中,添加测试Target;

然后就能看的的测试覆盖率

Eea1pt.png

以上就是对现有框架下UnitTest的简单实践。

你可能感兴趣的:(基于Moya + RxSwift + ReactorKit 框架下的 Unit Tests 探讨)