iOS网络请求模拟库OHHTTPStubs的介绍和使用
你能使用OHHTTPStubs做什么
OHHTTPStubs的主要功能有两点:
- 伪造网络请求返回的数据
- 模拟网络请求时的慢网环境
我们通常会在以下情况下使用:
- 伪造数据、模拟慢网环境,检测APP在网络环境不好的情况下的行为。
- 单元测试时,使用OHHTTPStubs提供模拟数据。
- 在开发阶段,模拟网络请求数据,不依赖服务器而进行本地开发,通过这样的方式来加速开发。
基本用法
- 拦截域名为
mywebservice.com
的http请求,并返回一个数组对象。
[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest * _Nonnull request) {
return [request.URL.host isEqualToString:@"mywebservice.com"];
} withStubResponse:^OHHTTPStubsResponse * _Nonnull(NSURLRequest * _Nonnull request) {
NSArray *array = @[@"Hello", @"world"];
return [OHHTTPStubsResponse responseWithJSONObject:array statusCode:200 headers:nil];
}];
- 向
mywebservice.com
发送GET请求,这个时候会收到成功的返回,并且responseObject
为数组[@"Hello", @"world"]
。
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"http://mywebservice.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable
responseObject) {
NSArray *data = responseObject;
NSLog(@"%@", data);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%@", error);
}];
可以看到,OHHTTPStubs的使用非常简单方便,设置需要拦截的URL,提供拦截到请求之后,提供需要返回的OHHTTPStubsResponse
对象。
OHHTTPStubs主要接口
- 添加stub
+(id<OHHTTPStubsDescriptor>)stubRequestsPassingTest:(OHHTTPStubsTestBlock)testBlock
withStubResponse:(OHHTTPStubsResponseBlock)responseBlock;
这是OHHTTPStubs
的类方法,我们通过调用这个方法来对URL进行拦截。
在参数testBlock
中进行URL的匹配,如果返回YES,则该请求将会被拦截。
在参数responseBlock
中,返回拦截了该请求之后的响应数据OHHTTPStubsResponse
对象。
-
OHHTTPStubsResponse
对象
OHHTTPStubsResponse
对象是作为被拦截的请求的响应,它包含HTTP响应头、响应正文、状态码、响应时间等信息。
OHHTTPStubsResponse
这个类提供了快捷方法来创建该对象。
- 使用
NSData
作为响应数据:
+(instancetype)responseWithData:(NSData*)data
statusCode:(int)statusCode
headers:(nullable NSDictionary*)httpHeaders;
- 使用文件的内容来作为响应数据:
+(instancetype)responseWithFileAtPath:(NSString *)filePath
statusCode:(int)statusCode
headers:(nullable NSDictionary*)httpHeaders;
+(instancetype)responseWithFileURL:(NSURL *)fileURL
statusCode:(int)statusCode
headers:(nullable NSDictionary *)httpHeaders;
- 使用JSON对象来作为响应数据:
+ (instancetype)responseWithJSONObject:(id)jsonObject
statusCode:(int)statusCode
headers:(nullable NSDictionary *)httpHeaders;
- 直接返回一个网络错误:
+(instancetype)responseWithError:(NSError*)error;
- 网络状况的模拟
网络状况的模拟主要是通过OHHTTPStubsResponse
对象的requestTime
和responseTime
两个属性来控制。
-
requestTime
:
请求在发送前必须等待的时间 -
responseTime
:
正数时,responseTime
就是从发送请求到接收到完整响应数据的时间。
负数时,responseTime
就是下载速度,会根据需要下载的数据的大小,动态的计算下载完成所需要的时间。
对于responseTime
,OHHTTPStubs已经定义了一些常量值供我们使用
const double OHHTTPStubsDownloadSpeed1KBPS =- 8 / 8; // kbps -> KB/s
const double OHHTTPStubsDownloadSpeedSLOW =- 12 / 8; // kbps -> KB/s
const double OHHTTPStubsDownloadSpeedGPRS =- 56 / 8; // kbps -> KB/s
const double OHHTTPStubsDownloadSpeedEDGE =- 128 / 8; // kbps -> KB/s
const double OHHTTPStubsDownloadSpeed3G =- 3200 / 8; // kbps -> KB/s
const double OHHTTPStubsDownloadSpeed3GPlus =- 7200 / 8; // kbps -> KB/s
const double OHHTTPStubsDownloadSpeedWifi =- 12000 / 8; // kbps -> KB/s
可以通过如下代码设置requestTime
和responseTime
:
[[OHHTTPStubsResponse responseWithData:data statusCode:200 headers:@{@"Content-Type":@"application/json"}]
requestTime:1.0f responseTime:OHHTTPStubsDownloadSpeed3G];
- 移除注册的stub
查看stubRequestsPassingTest:withStubResponse:
的源码
+(id<OHHTTPStubsDescriptor>)stubRequestsPassingTest:(OHHTTPStubsTestBlock)testBlock
withStubResponse:(OHHTTPStubsResponseBlock)responseBlock
{
OHHTTPStubsDescriptor* stub = [OHHTTPStubsDescriptor stubDescriptorWithTestBlock:testBlock
responseBlock:responseBlock];
[OHHTTPStubs.sharedInstance addStub:stub];
return stub;
}
+ (instancetype)sharedInstance
{
static OHHTTPStubs *sharedInstance = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
我们可以看到,每次添加一个stub之后,会创建一个OHHTTPStubsDescriptor
实例由单例对象OHHTTPStubs
持有。如果我们不手动将添加的stub移除,势必会造成内存常驻。我们可以调用以下方法移除添加的stub。
+(BOOL)removeStub:(id<OHHTTPStubsDescriptor>)stubDesc;
+(void)removeAllStubs;
- 启用和禁用stubs
所有的stubs默认是启用的,即默认会对需要拦截的请求进行拦截。可以通过以下方法启用或禁用拦截:
+(void)setEnabled:(BOOL)enabled;
+ (void)setEnabled:(BOOL)enabled forSessionConfiguration:(NSURLSessionConfiguration *)sessionConfig;
- 添加监听事件
/**
添加一个在每个stub被触发都执行一次的block
@param block 每个stub被触发都执行一次的block
*/
+(void)onStubActivation:( nullable void(^)(NSURLRequest* request, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub) )block;
/**
添加一个在OHHTTPStubs发生请求重定向的时候被执行的block
@param block 在OHHTTPStubs发生请求重定向的时候被执行的block
*/
+(void)onStubRedirectResponse:( nullable void(^)(NSURLRequest* request, NSURLRequest* redirectRequest, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub) )block;
/**
添加一个在stub完成之后会被执行的block
@param block 在stub完成之后会被执行的block
*/
+(void)afterStubFinish:( nullable void(^)(NSURLRequest* request, id<OHHTTPStubsDescriptor> stub, OHHTTPStubsResponse* responseStub, NSError *error) )block;
/**
添加一个在OHHTTPStubs遇到无法拦截的请求时调用的block
@param block 在OHHTTPStubs遇到无法拦截的请求时调用的block
*/
+(void)onStubMissing:( nullable void(^)(NSURLRequest* request) )block;
- POST请求时的HTTPBody
当使用NSURLSession
发送POST请求时,请求的body会在到达OHHTTPStubs
时被置为nil
。也就是说在testBlock
和responseBlock
中,直接获取NSURLRequest
的HTTPBody
会得到nil
。
OHHTTPStubs
通过方法置换在调用NSURLRequest
的setHTTPBody:
时,将HTTPBody
的内容做了一个备份。提供了方法OHHTTPStubs_HTTPBody
来获取备份的HTTPBody
。
具体代码如下:
NSString * const OHHTTPStubs_HTTPBodyKey = @"HTTPBody";
@implementation NSURLRequest (HTTPBodyTesting)
- (NSData*)OHHTTPStubs_HTTPBody
{
return [NSURLProtocol propertyForKey:OHHTTPStubs_HTTPBodyKey inRequest:self];
}
@end
#pragma mark - NSMutableURLRequest+HTTPBodyTesting
typedef void(*OHHHTTPStubsSetterIMP)(id, SEL, id);
static OHHHTTPStubsSetterIMP orig_setHTTPBody;
static void OHHTTPStubs_setHTTPBody(id self, SEL _cmd, NSData* HTTPBody)
{
// store the http body via NSURLProtocol
if (HTTPBody) {
[NSURLProtocol setProperty:HTTPBody forKey:OHHTTPStubs_HTTPBodyKey inRequest:self];
} else {
// unfortunately resetting does not work properly as the NSURLSession also uses this to reset the property
}
orig_setHTTPBody(self, _cmd, HTTPBody);
}
/**
* Swizzles setHTTPBody: in order to maintain a copy of the http body for later
* reference and calls the original implementation.
*
* @warning Should not be used in production, testing only.
*/
@interface NSMutableURLRequest (HTTPBodyTesting) @end
@implementation NSMutableURLRequest (HTTPBodyTesting)
+ (void)load
{
orig_setHTTPBody = (OHHHTTPStubsSetterIMP)OHHTTPStubsReplaceMethod(@selector(setHTTPBody:),
(IMP)OHHTTPStubs_setHTTPBody,
[NSMutableURLRequest class],
NO);
}
@end
注意事项
-
OHHTTPStubs
不能用于后台会话(由[NSURLSessionConfiguration backgroundSessionConfiguration]
创建的会话),因为后台会话由iOS系统自己维护,并且不允许使用NSURLProtocols
。 -
OHHTTPStubs
不能模拟数据上传。NSURLProtocolClient
协议并没有提供任何方式通知delegate数据已经发送,所以一个NSURLRequest
的HTTPBody
、HTTPBodyStream
或是由-[NSURLSession uploadTaskWithRequest:fromData:]
提供的数据都会被忽略。最重要的是使用OHHTTPStubs
来stub一个请求后,代理方法-URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
永远也不会被调用。 -
OHHTTPStubs
还有一个重定向的问题,具有零延迟的重定向会以一个空响应结束。
提交到App Store
OHHTTPStubs
中没有使用任何私有的API,它是可以被提交到App Store的。但是我们基本上只会在开发阶段使用stubs,所以是没有必要把stubs提交到App Store的。
我们可以通过#if DEBUG
块的方式来避免提交相关代码到App Store。
为了避免在发布前忘记移除对OHHTTPStubs
的导入,可以使用下面的方式对pod进行配置:
pod 'OHHTTPStubs', :configurations => ['Debug', 'Development']
或以下代码:
pod 'OHHTTPStubs', :configurations => 'Debug'
如果配置为仅在Debug
模式下导入OHHTTPStubs
,那么在其它模式下使用OHHTTPStubs
就会报错。所以一定要记得使用#if DEBUG
块或者删除相关的代码。
最后附上github地址:https://github.com/AliSoftware/OHHTTPStubs