这篇文章会对 OHHTTPStubs 源代码的分析,其实现原理是建立在 NSURLProtocol 的基础上的,对这部分内容不了解的读者,可以阅读这篇文章 iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求了解相关知识,本文中不会介绍拦截 HTTP 请求的原理。

如何使用 OHHTTPStubs Mock 网络请求

  HTTP Mock 在测试中非常好用,我们可以在不需要后端 API 的情况下,在本地对 HTTP 请求进行拦截,返回想要的 json数据,而 OHHTTPStubs 就为我们提供了这样一种解决方案。

  在了解其实现之前,先对 OHHTTPStubs 进行简单的介绍,引入头文件这种事情在这里会直接省略,先来看一下程序的源代码:

[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest * _Nonnull request) {
return [request.URL.absoluteString isEqualToString:@"https://idont.know"];
} withStubResponse:^OHHTTPStubsResponse * _Nonnull(NSURLRequest * _Nonnull request) {
NSString *fixture = OHPathForFile(@"example.json", self.class);
return [OHHTTPStubsResponse responseWithFileAtPath:fixture statusCode: headers:@{@"Content-Type":@"application/json"}];
}]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"https://idont.know"
parameters:nil
progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"%@", responseObject);
} failure:nil];

  我们向 https://idont.know 这个 URL 发送一个 GET 请求,虽然这个 URL 并不存在,但是这里的代码通过 HTTP stub 成功地模拟了 HTTP 响应:

OHHTTPStubs 的实现

  在了解了 OHHTTPStubs 的使用之后,我们会对其实现进行分析,它分成四部分进行:

  • OHHTTPStubsProtocol 拦截 HTTP 请求
  • OHHTTPStubs 单例管理 OHHTTPStubsDescriptor 实例
  • OHHTTPStubsResponse 伪造 HTTP 响应
  • 一些辅助功能

OHHTTPStubsProtocol 拦截 HTTP 请求

  在 OHHTTPStubs 中继承 NSURLProtocol 的类就是 OHHTTPStubsProtocol,它在 HTTP 请求发出之前对 request 对象进行过滤以及处理:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
return ([OHHTTPStubs.sharedInstance firstStubPassingTestForRequest:request] != nil);
} - (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)response client:(id<NSURLProtocolClient>)client {
OHHTTPStubsProtocol* proto = [super initWithRequest:request cachedResponse:nil client:client];
proto.stub = [OHHTTPStubs.sharedInstance firstStubPassingTestForRequest:request];
return proto;
} + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}

  判断请求是否会被当前协议对象进行处理是需要 OHHTTPStubs 的实例方法 - firstStubPassingTestForRequest: 的执行的,在这里暂时先不对这个方法进行讨论。

  接下来就是请求发送的过程 - startLoading 方法了,该方法的实现实在是太过于复杂,所以这里分块来分析代码:

- (void)startLoading {
NSURLRequest* request = self.request;
id<NSURLProtocolClient> client = self.client; OHHTTPStubsResponse* responseStub = self.stub.responseBlock(request); if (OHHTTPStubs.sharedInstance.onStubActivationBlock) {
OHHTTPStubs.sharedInstance.onStubActivationBlock(request, self.stub, responseStub);
} ...
}

  从当前对象中取出 request 以及 client 对象,如果 OHHTTPStubs 的单例中包含 onStubActivationBlock,就会执行这里的 block,然后调用 responseBlock 获取一个 OHHTTPStubsResponse HTTP 响应对象。

  OHHTTPStubs 不只提供了 onStubActivationBlock 这一个钩子,还有以下 block:

  • + onStubActivationBlock:stub 被激活时调用
  • + onStubRedirectBlock:发生重定向时
  • + afterStubFinishBlock:在 stub 结束时调用

  如果响应对象的生成没有遇到任何问题,就会进入处理 Cookie、重定向、发送响应和模拟数据流的过程了。

  1. 首先是对 Cookie 的处理
NSHTTPURLResponse* urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL
statusCode:responseStub.statusCode
HTTPVersion:@"HTTP/1.1"
headerFields:responseStub.httpHeaders]; if (request.HTTPShouldHandleCookies && request.URL) {
NSArray* cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseStub.httpHeaders forURL:request.URL];
if (cookies) {
[NSHTTPCookieStorage.sharedHTTPCookieStorage setCookies:cookies forURL:request.URL mainDocumentURL:request.mainDocumentURL];
}
}
  1. 如果 HTTP 状态码在 300-400 之间,就会处理重定向的问题,调用 onStubRedirectBlock 进行需要的回调
NSString* redirectLocation = (responseStub.httpHeaders)[@"Location"];
NSURL* redirectLocationURL = redirectLocation ? [NSURL URLWithString:redirectLocation] : nil; if (((responseStub.statusCode > ) && (responseStub.statusCode < )) && redirectLocationURL) {
NSURLRequest* redirectRequest = [NSURLRequest requestWithURL:redirectLocationURL];
[self executeOnClientRunLoopAfterDelay:responseStub.requestTime block:^{
if (!self.stopped) {
[client URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:urlResponse];
if (OHHTTPStubs.sharedInstance.onStubRedirectBlock) {
OHHTTPStubs.sharedInstance.onStubRedirectBlock(request, redirectRequest, self.stub, responseStub);
}
}
}];
}
  1. 最后这里有一些复杂,我们根据 stub 中存储的 responseTime 来模拟响应的一个延迟时间,然后使用 - streamDataForClient:withStubResponse:completion: 来模拟数据以 NSData 的形式分块发送回 client 的过程,最后调用 afterStubFinishBlock
[self executeOnClientRunLoopAfterDelay:responseStub.requestTime block:^{
if (!self.stopped) {
[client URLProtocol:self didReceiveResponse:urlResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed];
if(responseStub.inputStream.streamStatus == NSStreamStatusNotOpen) {
[responseStub.inputStream open];
}
[self streamDataForClient:client
withStubResponse:responseStub
completion:^(NSError * error) {
[responseStub.inputStream close];
NSError *blockError = nil;
if (error==nil) {
[client URLProtocolDidFinishLoading:self];
} else {
[client URLProtocol:self didFailWithError:responseStub.error];
blockError = responseStub.error;
}
if (OHHTTPStubs.sharedInstance.afterStubFinishBlock) {
OHHTTPStubs.sharedInstance.afterStubFinishBlock(request, self.stub, responseStub, blockError);
}
}];
}
}];

  当然如果在生成 responseStub 的时候发生了错误,也会进行类似的操作,在延迟一定时间(模拟网络延迟)后执行 block 并传入各种参数:

[self executeOnClientRunLoopAfterDelay:responseStub.responseTime block:^{
if (!self.stopped) {
[client URLProtocol:self didFailWithError:responseStub.error];
if (OHHTTPStubs.sharedInstance.afterStubFinishBlock) {
OHHTTPStubs.sharedInstance.afterStubFinishBlock(request, self.stub, responseStub, responseStub.error);
}
}
}];

模拟数据流

  因为在客户端接收数据时,所有的 NSData 并不是一次就涌入客户端的,而是分块加载打包解码的,尤其是在我们执行下载操作时,有时几 MB 的文件不可能同时到达服务端,而 - startLoading 中调用的 - streamDataForClient:withStubResponse:completion: 方法就是为了模拟数据流,分块向服务端发送数据,不过这部分的处理涉及到一个私有的结构体 OHHTTPStubsStreamTimingInfo

typedef struct {
NSTimeInterval slotTime;
double chunkSizePerSlot;
double cumulativeChunkSize;
} OHHTTPStubsStreamTimingInfo;

  这个结构体包含了关于发送数据流的信息:

  • slotTime:两次发送 NSData 的间隔时间
  • chunkSizePerSlot:每块数据流大小
  • cumulativeChunkSize:已发送的数据流大小

  模拟数据流的过程需要两个方法的支持,其中一个方法做一些预加载工作:

- (void)streamDataForClient:(id<NSURLProtocolClient>)client
withStubResponse:(OHHTTPStubsResponse*)stubResponse
completion:(void(^)(NSError * error))completion {
if ((stubResponse.dataSize>) && stubResponse.inputStream.hasBytesAvailable && (!self.stopped)) {
OHHTTPStubsStreamTimingInfo timingInfo = {
.slotTime = kSlotTime,
.cumulativeChunkSize =
}; if(stubResponse.responseTime < ) {
timingInfo.chunkSizePerSlot = (fabs(stubResponse.responseTime) * ) * timingInfo.slotTime;
} else if (stubResponse.responseTime < kSlotTime) {
timingInfo.chunkSizePerSlot = stubResponse.dataSize;
timingInfo.slotTime = stubResponse.responseTime;
} else {
timingInfo.chunkSizePerSlot = ((stubResponse.dataSize/stubResponse.responseTime) * timingInfo.slotTime);
} [self streamDataForClient:client
fromStream:stubResponse.inputStream
timingInfo:timingInfo
completion:completion];
} else {
if (completion) completion(nil);
}
}

  该方法将生成的 OHHTTPStubsStreamTimingInfo 信息传入下一个实例方法 - streamDataForClient:fromStream:timingInfo:completion:

- (void)streamDataForClient:(id<NSURLProtocolClient>)client fromStream:(NSInputStream*)inputStream timingInfo:(OHHTTPStubsStreamTimingInfo)timingInfo completion:(void(^)(NSError * error))completion {
if (inputStream.hasBytesAvailable && (!self.stopped)) {
double cumulativeChunkSizeAfterRead = timingInfo.cumulativeChunkSize + timingInfo.chunkSizePerSlot;
NSUInteger chunkSizeToRead = floor(cumulativeChunkSizeAfterRead) - floor(timingInfo.cumulativeChunkSize);
timingInfo.cumulativeChunkSize = cumulativeChunkSizeAfterRead; if (chunkSizeToRead == ) {
[self executeOnClientRunLoopAfterDelay:timingInfo.slotTime block:^{
[self streamDataForClient:client fromStream:inputStream
timingInfo:timingInfo completion:completion];
}];
} else {
uint8_t* buffer = (uint8_t*)malloc(sizeof(uint8_t)*chunkSizeToRead);
NSInteger bytesRead = [inputStream read:buffer maxLength:chunkSizeToRead];
if (bytesRead > ) {
NSData * data = [NSData dataWithBytes:buffer length:bytesRead];
[self executeOnClientRunLoopAfterDelay:((double)bytesRead / (double)chunkSizeToRead) * timingInfo.slotTime block:^{
[client URLProtocol:self didLoadData:data];
[self streamDataForClient:client fromStream:inputStream
timingInfo:timingInfo completion:completion];
}];
} else {
if (completion) completion(inputStream.streamError);
}
free(buffer);
}
} else {
if (completion) completion(nil);
}
}
  • 上述方法会先计算 chunkSizeToRead,也就是接下来要传递给 client 的数据长度
  • 从 NSInputStream 中读取对应长度的数据
  • 通过 - executeOnClientRunLoopAfterDelay:block: 模拟数据传输的延时
  • 使用 - URLProtocol:didLoadData: 代理方法将数据传回 client

  OHHTTPStubs 通过上面的两个方法很好的模拟了 HTTP 响应由于网络造成的延迟以及数据分块到达客户端的特点。

OHHTTPStubs 以及 OHHTTPStubsDescriptor 对 stub 的管理

  OHHTTPStubs 遵循单例模式,其主要作用就是提供便利的 API 并持有一个 OHHTTPStubsDescriptor 数组,对 stub 进行管理。

  OHHTTPStubs 提供的类方法 + stubRequestsPassingTest:withStubResponse: 会添加一个 OHHTTPStubsDescriptor 的实例到 OHHTTPStubsDescriptor 数组中:

+ (id<OHHTTPStubsDescriptor>)stubRequestsPassingTest:(OHHTTPStubsTestBlock)testBlock
withStubResponse:(OHHTTPStubsResponseBlock)responseBlock {
OHHTTPStubsDescriptor* stub = [OHHTTPStubsDescriptor stubDescriptorWithTestBlock:testBlock
responseBlock:responseBlock];
[OHHTTPStubs.sharedInstance addStub:stub];
return stub;
}

  该类主要有两种方法,一种方法用于管理持有的 HTTP stub,比如说:

  • + (BOOL)removeStub:(id<OHHTTPStubsDescriptor>)stubDesc
  • + (void)removeAllStubs
  • - (void)addStub:(OHHTTPStubsDescriptor*)stubDesc
  • - (BOOL)removeStub:(id<OHHTTPStubsDescriptor>)stubDesc
  • - (void)removeAllStubs

  这些方法都是用来操作单例持有的数组的,而另一种方法用来设置相应事件发生时的回调:

  • + (void)onStubActivation:( nullable void(^)(NSURLRequest* request, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub) )block
  • + (void)onStubRedirectResponse:( nullable void(^)(NSURLRequest* request, NSURLRequest* redirectRequest, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub) )block
  • + (void)afterStubFinish:( nullable void(^)(NSURLRequest* request, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub, NSError* error) )block

  类中最重要的实例方法就是 - firstStubPassingTestForRequest:,它遍历自己持有的全部 stub,通过 testBlock 的调用返回第一个符合条件的 stub:

- (OHHTTPStubsDescriptor*)firstStubPassingTestForRequest:(NSURLRequest*)request {
OHHTTPStubsDescriptor* foundStub = nil;
@synchronized(_stubDescriptors) {
for(OHHTTPStubsDescriptor* stub in _stubDescriptors.reverseObjectEnumerator) {
if (stub.testBlock(request)) {
foundStub = stub;
break;
}
}
}
return foundStub;
}

  相比之下 OHHTTPStubsDescriptor 仅仅作为一个保存信息的类,其职能相对单一、实现相对简单:

@interface OHHTTPStubsDescriptor : NSObject <OHHTTPStubsDescriptor>
@property(atomic, copy) OHHTTPStubsTestBlock testBlock;
@property(atomic, copy) OHHTTPStubsResponseBlock responseBlock;
@end @implementation OHHTTPStubsDescriptor + (instancetype)stubDescriptorWithTestBlock:(OHHTTPStubsTestBlock)testBlock
responseBlock:(OHHTTPStubsResponseBlock)responseBlock {
OHHTTPStubsDescriptor* stub = [OHHTTPStubsDescriptor new];
stub.testBlock = testBlock;
stub.responseBlock = responseBlock;
return stub;
} @end

  两个属性以及一个方法构成了 OHHTTPStubsDescriptor 类的全部实现。

OHHTTPStubsResponse 伪造 HTTP 响应

  OHHTTPStubsResponse 类为请求提供了相应所需要的各种参数,HTTP 状态码、请求时间以及数据的输入流也就是用于模拟网络请求的 inputStream

  指定构造器 - initWithFileURL:statusCode:headers: 完成了对这些参数的配置:

- (instancetype)initWithInputStream:(NSInputStream*)inputStream dataSize:(unsigned long long)dataSize statusCode:(int)statusCode headers:(nullable NSDictionary*)httpHeaders {
if (self = [super init]) {
_inputStream = inputStream;
_dataSize = dataSize;
_statusCode = statusCode;
NSMutableDictionary * headers = [NSMutableDictionary dictionaryWithDictionary:httpHeaders];
static NSString *const ContentLengthHeader = @"Content-Length";
if (!headers[ContentLengthHeader]) {
headers[ContentLengthHeader] = [NSString stringWithFormat:@"%llu",_dataSize];
}
_httpHeaders = [NSDictionary dictionaryWithDictionary:headers];
}
return self;
}

  同时,该类也提供了非常多的便利构造器以及类方法帮助我们实例化 OHHTTPStubsResponse,整个类中的所有构造方法大都会调用上述构造器;只是会传入不同的参数:

- (instancetype)initWithFileURL:(NSURL *)fileURL statusCode:(int)statusCode headers:(nullable NSDictionary *)httpHeaders {
NSNumber *fileSize;
NSError *error;
const BOOL success __unused = [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:&error]; return [self initWithInputStream:[NSInputStream inputStreamWithURL:fileURL] dataSize:[fileSize unsignedLongLongValue] statusCode:statusCode headers:httpHeaders];
}

  比如 - initWithFileURL:statusCode:headers: 方法就会从文件中读取数据,然后构造一个数据输入流。

其他内容

  使用 NSURLProtocol 拦截 HTTP 请求时会有一个非常严重的问题,如果发出的是 POST 请求,请求的 body 会在到达 OHHTTPStubs 时被重置为空,也就是我们无法直接在 testBlock 中获取其 HTTPBody;所以,我们只能通过通过方法调剂在设置 HTTPBody 时,进行备份:

typedef void(*OHHHTTPStubsSetterIMP)(id, SEL, id);
static OHHHTTPStubsSetterIMP orig_setHTTPBody; static void OHHTTPStubs_setHTTPBody(id self, SEL _cmd, NSData* HTTPBody) {
if (HTTPBody) {
[NSURLProtocol setProperty:HTTPBody forKey:OHHTTPStubs_HTTPBodyKey inRequest:self];
}
orig_setHTTPBody(self, _cmd, HTTPBody);
}
@interface NSMutableURLRequest (HTTPBodyTesting) @end @implementation NSMutableURLRequest (HTTPBodyTesting) + (void)load {
orig_setHTTPBody = (OHHHTTPStubsSetterIMP)OHHTTPStubsReplaceMethod(@selector(setHTTPBody:), (IMP)OHHTTPStubs_setHTTPBody, [NSMutableURLRequest class], NO);
} @end

  除了对于 HTTPBody 的备份之外,OHHTTPStubs 还提供了一些用于从文件中获取数据的 C 函数:

NSString* __nullable OHPathForFile(NSString* fileName, Class inBundleForClass);
NSString* __nullable OHPathForFileInBundle(NSString* fileName, NSBundle* bundle);
NSString* __nullable OHPathForFileInDocumentsDir(NSString* fileName);
NSBundle* __nullable OHResourceBundle(NSString* bundleBasename, Class inBundleForClass);

总结

  如果阅读过上一篇文章中的内容,理解这里的实现原理也不是什么太大的问题。在需要使用到 HTTP mock 进行测试时,使用 OHHTTPStubs 还是很方便的,当然现在也有很多其他的 HTTP stub 框架,不过实现基本上都是基于 NSURLProtocol的。

转载自

https://github.com/Draveness/analyze

iOS进阶之如何进行 HTTP Mock(转载)的更多相关文章

  1. iOS进阶_地图上定位的标志——大头针

    一.添加大头针 地图使用的框架是MapKit 大头针走的是MKAnnotation协议 /* 注意:因为是满足协议MKAnnotation,所以没有MKAnnotation的系统大头针类,必须自定义大 ...

  2. iOS进阶指南试读之UI篇

    iOS进阶指南试读之UI篇 UI篇 UI是一个iOS开发工程师的基本功.怎么说?UI本质上就是你调用苹果提供给你的API来完成设计师的设计.所以,想提升UI的功力也很简单,没事就看看UIKit里的各个 ...

  3. iOS进阶之使用 NSURLProtocol 拦截 HTTP 请求(转载)

    这篇文章会提供一种在 Cocoa 层拦截所有 HTTP 请求的方法,其实标题已经说明了拦截 HTTP 请求需要的了解的就是 NSURLProtocol. 由于文章的内容较长,会分成两部分,这篇文章介绍 ...

  4. iOS进阶读物

    不知不觉作为 iOS 开发也有两年多的时间了,记得当初看到 OC 的语法时,愣是被吓了回去,隔了好久才重新耐下心去啃一啃.啃了一阵,觉得大概有了点概念,看到 Cocoa 那么多的 Class,又懵了, ...

  5. iOS进阶推荐的书目

    <Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法>([英]Matt Galloway) 很多面试题有涉及 <IOS数据库应用高级编 ...

  6. iOS进阶

    著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处.作者:wjh2005链接:https://www.zhihu.com/question/28518265/answer/887505 ...

  7. iOS进阶之页面性能优化

    转载:http://www.jianshu.com/p/1b5cbf155b31 前言 在软件开发领域里经常能听到这样一句话,"过早的优化是万恶之源",不要过早优化或者过度优化.我 ...

  8. 移动端IOS和androi及浏览器js判断[转载]

    转载自:http://www.niutifa.com/?p=561 移动端IOS和androi及浏览器js判断: <script type="text/javascript" ...

  9. iOS多线程编程之NSThread的使用(转载)

    1.简介: 1.1 iOS有三种多线程编程的技术,分别是: 1.NSThread 2.Cocoa NSOperation (iOS多线程编程之NSOperation和NSOperationQueue的 ...

随机推荐

  1. pta编程总结

    币值转换 (20 分) 输入一个整数(位数不超过9位)代表一个人民币值(单位为元),请转换成财务要求的大写中文格式.如23108元,转换后变成“贰万叁仟壹百零捌”元.为了简化输出,用小写英文字母a-j ...

  2. IIC稳定性.VBS

    Sub Main Dim cnt Dim delay Dim time Dim atttime atttime = 20 delay = 3000 time = 50 crt.screen.Send ...

  3. Python 学习笔记9 循环语句 For in

    For in 循环主要适用于遍历一个对象中的所有元素.我们可以使用它遍历列表,元组和字典等等. 其主要的流程如下:(图片来源于: https://www.yiibai.com/python/pytho ...

  4. python learning 字符串方法

    一.重点掌握的6种字符串方法: 1.join命令 功能:用于合并,将字符串中的每一个元素按照指定分隔符进行拼接 程序举例: seq = ['1','2','3','4'] sep = '+' v = ...

  5. Springboot-001-解决nested exception is org.apache.ibatis.binding.BindingException: Parameter 'env' not found. Available parameters are [arg1, arg0, param1, param2]

    环境:Springboot + Mybatis + MySQL + VUE 场景: 前端发出数据比对请求,在服务后台与数据库交互时,接口提示错误信息如下所示: { "code": ...

  6. 将dataframe分割为训练集和测试集两部分

    data = pd.read_csv("./dataNN.csv",',',error_bad_lines=False)#我的数据集是两列,一列字符串,一列为0,1的labelda ...

  7. lucene基础

    同一个域中,即使相同的单词,如出现两次JAVA,也是不同的token,但他们对应相同的term,在term中记录这些token信息 数据库数据,与luence数据 需要搜寻(也即索引)的field,存 ...

  8. 2019春第五周作业Compile Summarize

    这个作业属于哪个课程 C语言程序设计II 这个作业要求在哪里 在这里 我在这个课程的目标是 能够精通关于数组内部运作原理 这个作业在哪个具体方面帮助我实现目标 如何输出一行的连续字符 参考文献与网址 ...

  9. C# 字典Dictionary

    Dictionary<TKey, TValue> 泛型类提供了从一组键到一组值的映射.通过键来检索值的速度是非常快的,接近于 O(1),这是因为 Dictionary<TKey, T ...

  10. 使用 notify.js 桌面提醒

    //var iN = new iNotify({ // effect: 'flash', // interval: 500, // message: "有消息拉!", // aud ...