基于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<NetworkTarget>(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<NetworkTarget>(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>中发生的所有事件,转换成一个[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<NetworkTarget>(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<ProductInfo>().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<NetworkTarget>(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的简单实践。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345