iOS App Mock方案

一.为什么需要Mock

在app开发过程中,有时候server端并未开发完成,接口未能实现,这个时候又要求客户端同步进行开发;另外,就算接口已经开放完成,但是返回的数据,也未必能满足移动端的测试要求;如何破?1.可以通过hardcode来临时解决,但是这个很容易出错,在正式发版的时候还需要找到所有的hardcode位置改过来,否则后果严重啊。2.还有更“高级”一点,通过设置代理,用抓包工具修改网络数据,但这种效率低得令人抓狂。
引入一个可以模拟网络请求的工具似乎就可以轻松满足需求,但实践证明,“模拟网络请求”这个需求并不简单。例如对于全新的业务,后台如果还没有数据,前端完全可以根据协议自己制造假数据返回。但是,很多情况下,可能是对已有业务的变更,也就是需要修改后台已有的业务数据。

业界解决方案

为了满足开发过程中模拟网络请求的需求,HttpMock 工具应运而生,目前业界已经有许多不同的实现方式,基本可以分为两类:

自建HTTP Server

可以在本地搭建 HTTP Server 模拟返回客户端所需要的数据。以 hibri/HttpMock 为例,它就是在本地搭建了一个HTTP Mock Server,然后根据需求返回指定数据。对于不需要模拟的请求,直接到达真实的Server,需要模拟的请求就转向MockServer。
iOS 上目前应用比较广泛的是 OHHTTPStubs 和 Nocilla ,这两种实现的功能都类似。Nocilla选择用领域专用语言(DSL)的形式创建模拟请求,更容易理解,但是mock的功能需要应用中主动开启和关闭,一旦开启或关闭会影响应用中所有的HTTP请求。OHHTTPStubs 安装后自动启动,根据 request 自动判断是否需要截获。但目前这些开源库都未能做到灵活修改网络返回的数据。

GYHttpMock 优势

GYHttpMock 采用客户端截获的方式,在 Nocilla DSL 特性基础上,同时学习OHHTTPStubs的自动开启和识别,实现了 http response 的部分替换功能。
具体优势:
支持部分替换 HTTP Response,也就是可以修改真实网络返回的数据,这是相对于其它 HttpMock 独有的核心功能。客户端引入 GYHttpMock 后,只需一行代码就可以截获指定请求,并返回所需要的数据。不需服务端支持,也不需要建立本地HTTP Server。支持 NSURLConnection, NSURLSession,AFNetworking 以及所有采用 iOS Cocoa URL 加载方式的网络框架。支持正则匹配 HTTP Request,这样一条 httpMock 可以同时支持多个请求。mocked response 支持 json 内容的文件。一般情况下,mocked response 直接用 NSString 表达会比较清晰,但是返回内容比较多的情况下,因为转义符的原因,将内容以 json 格式写入文件会更容易些。使用安装
直接将 GYHttpMock 的源文件加入项目中即可。也可以通过 CocoaPods 的方式接入。

应用

在需要拦截的请求之前创建正确的mockRequest:
1.创建一个最简单的 mockRequest。截获应用中访问 www.weread.com 的 get 请求,并返回一个 response body为空的数据。

mockRequest(@"GET", @"http://www.weread.com"); 

2.创建一个拦截条件更复杂的 mockRequest。截获应用中 url 包含 weread.com,而且包含了 name=abc 的参数

mockRequest(@"GET", @"(.*?)weread.com(.*?)".regex). 
withBody(@"{/"name/":/"abc/"}".regex); 

3.创建一个指定返回数据的 mockRequest。withBody的值也可以是某个 xxx.json 文件,不过这个 json 文件需要加入到项目中。

mockRequest(@"POST", @"http://www.weread.com"). withBody(@"{/"name/":/"abc/"}".regex); 
andReturn(200). withBody(@"{/"key/":/"value/"}"); 

经测试,这个表示好像有问题。没有通过编译。
4.创建一个修改部分返回数据的 mockRequest。这里会根据 weread.json 的内容修改正常网络返回的数据

mockRequest(@"POST", @"http://www.weread.com"). 
isUpdatePartResponseBody(YES). 
withBody(@"{/"name/":/"abc/"}".regex); 
andReturn(200). 
withBody(@“weread.json"); 

下面的代码经过测试,a1.json要放到.app包里

mockRequest(@"GET", @"https://www.baidu.com").withBody(@"").andReturn(200).withBody(@"a1.json");

假设正常网络返回的原始数据是这样:

{
    "data": [{
        "bookId": "0000001",
        "updated": [{
                "chapterIdx": 1,
                "title": "序言"
            },
            {
                "chapterIdx": 2,
                "title": "第2章"
            }
        ]
    }]
}

weread.json 的内容是这样:

{
    "data": [{
        "updated": [{
            "hello": "world"
        }]
    }]
}

修改后的数据就会就成这样:

{
    "data": [{
        "bookId": "0000001",
        "updated": [{
                "chapterIdx": 1,
                "title": "序言",
                "hello": "world"
            },
            {
                "chapterIdx": 2,
                "title": "第2章",
                "hello": "world"
            }
        ]
    }]
}

GYHttpMock会根据 weread.json 指定的层次结构来修改原始数据,前提是 wearied.json 的数据结构需要和正常的返回数据一致,否则会导致修改失败或者不可预知的错误。

实现原理

GYHttpMock的工作流程如下:


2539a157062493c1393a14d056616604.jpg

其核心实现主要包括request匹配、request拦截、response替换三个部分。

request匹配

用于判断应用中的某个HTTP Request是否应该被mock。判断的条件包括method、URL、Headers、Body,其中URL和Body都支持正规匹配的方式,一个httpMock可以同时匹配多个HTTP Request。

request拦截

request拦截是通过继承 NSURLProtocol 的子类来实现。 NSURLProtocol 是iOS URL网络加载中功能非常强大的一个类,官方文档也有说明 NSURLProtocol ,通过重写它的方法,可以重新定义系统网络加载行为。在此之前,对于 NSURLConnection 的网络请求,需要这样注册 NSURLProtocol 的子类 GYMockURLProtocol

[NSURLProtocol registerClass:[GYMockURLProtocol class]]; 

对于 NSURLSession 的网络请求,需要替换 protocolClasses 方法

Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration"); 
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];

最后,重点是重写 NSURLProtocol 类的 canInitWithRequest 和 startLoading 方法。 canInitWithRequest 是用于判断是否可以发起网络请求,可以通过这个过滤不在拦截范围内的request,不影响App的正常网络请求。 startLoading 是替换response数据的核心所在,成功截拦的request会进入该方法,在这个方法中替换或修改response数据,再回调给上层。

response替换

对于需要全部替换的response,实现方式是在 startLoading 方法中调中 NSURLProtocol 的 URLProtocol:didReceiveResponse:cacheStoragePolicy: 方法,将替换好的response回调给上层。对于需要部分替换的response,GYHttpMock会用NSURLConnection的方式,发起一次真正的网络请求,待数据回来后,再与mockRequest中的response数据进行合并,最后将合并后的数据回调上层。部分替换过程中遇到两个问题:
部分替换时要发出一个真实网络请求拿到原始数据,这个请求按照之前的规则又会被NSURLProtocol截获,从而进入死循环。解决办法是,start request前将这个GYHttpRequest打上标记,表明是不需要再次截获的,等拿到reponse后再将GYHttpRequest上的标记去掉,避免死循环。
两个response内容合并的问题。因为json的数据结构非常灵活,可以任意层次嵌套,如何指定修改或添加某个节点下的数据是比较困难的,尤其是json中数组的嵌套,导致要指定修改数组中某个位置的元素变得非常困难。GYHttpMock采用的方式是,在mockRequest的response中指出需要修改的节点完整位置,然后用这个数据结构去匹配目标数据(具体算法请查看 GYHttpMock源码 ,好处在于可以支持比较复杂的数据结构,但这就要求使用者对目标数据结构非常清楚。

GYHttpMock已经在 GitHub 开源,目前已用于 微信读书 项目中,使用过程如果有问题或者建议,欢迎提交 issue 和 pull request。

Demo稍后放到Github上面

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

推荐阅读更多精彩内容