OHHTTPStubs 源码解析

NSURLProtocol

NSURLProtocol 是苹果为我们提供的 URL Loading System 的一部分,这是一张从官方文档贴过来的图片:

URL-loading-syste

官方文档对 NSURLProtocol 的描述是这样的:

An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports.

在每一个 HTTP 请求开始时,URL 加载系统创建一个合适的 NSURLProtocol 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 NSURLProtocol 的类,并通过 - registerClass: 方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。

这样,我们需要解决的核心问题就变成了如何使用 NSURLProtocol 来处理所有的网络请求,这里使用苹果官方文档中的 CustomHTTPProtocol 进行介绍,你可以点击这里下载源代码。

在这个工程中 CustomHTTPProtocol.m 是需要重点关注的文件,CustomHTTPProtocol 就是 NSURLProtocol 的子类:

@interface CustomHTTPProtocol : NSURLProtocol

...

@end

现在重新回到需要解决的问题,也就是 如何使用 NSURLProtocol 拦截 HTTP 请求?,有这个么几个问题需要去解决:

  • 如何决定哪些请求需要当前协议对象处理?
  • 对当前的请求对象需要进行哪些处理?
  • NSURLProtocol 如何实例化?
  • 如何发出 HTTP 请求并且将响应传递给调用者?

上面的这几个问题其实都可以通过 NSURLProtocol 为我们提供的 API 来解决,决定请求是否需要当前协议对象处理的方法是:+ canInitWithRequest

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    BOOL shouldAccept;
    NSURL *url;
    NSString *scheme;

    shouldAccept = (request != nil);
    if (shouldAccept) {
        url = [request URL];
        shouldAccept = (url != nil);
    }
    return shouldAccept;
}

因为项目中的这个方法是大约有 60 多行,在这里只粘贴了其中的一部分,只为了说明该方法的作用:每一次请求都会有一个 NSURLRequest 实例,上述方法会拿到所有的请求对象,我们就可以根据对应的请求选择是否处理该对象;而上面的代码只会处理所有 URL 不为空的请求。

请求经过 + canInitWithRequest: 方法过滤之后,我们得到了所有要处理的请求,接下来需要对请求进行一定的操作,而这都会在 + canonicalRequestForRequest: 中进行,虽然它与 + canInitWithRequest: 方法传入的 request 对象都是一个,但是最好不要在 + canInitWithRequest: 中操作对象,可能会有语义上的问题;所以,我们需要覆写 + canonicalRequestForRequest:方法提供一个标准的请求对象:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

这里对请求不做任何修改,直接返回,当然你也可以给这个请求加个 header,只要最后返回一个 NSURLRequest 对象就可以。

在得到了需要的请求对象之后,就可以初始化一个 NSURLProtocol 对象了:

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {
    return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}

在这里直接调用 super 的指定构造器方法,实例化一个对象,然后就进入了发送网络请求,获取数据并返回的阶段了:

- (void)startLoading {
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[[NSURLSessionConfiguration alloc] init] delegate:self delegateQueue:nil];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:self.request];
    [task resume];
}

这里使用简化了 CustomHTTPClient 中的项目代码,可以达到几乎相同的效果。

你可以在 - startLoading 中使用任何方法来对协议对象持有的 request 进行转发,包括 NSURLSession、 NSURLConnection甚至使用 AFNetworking 等网络库,只要你能在回调方法中把数据传回 client,帮助其正确渲染就可以,比如这样:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];

    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [[self client] URLProtocol:self didLoadData:data];
}

当然这里省略后的代码只会保证大多数情况下的正确执行,只是给你一个对获取响应数据粗略的认知,如果你需要更加详细的代码,我觉得最好还是查看一下 CustomHTTPProtocol 中对 HTTP 响应处理的代码,也就是 NSURLSessionDelegate协议实现的部分。

client 你可以理解为当前网络请求的发起者,所有的 client 都实现了 NSURLProtocolClient 协议,协议的作用就是在 HTTP 请求发出以及接受响应时向其它对象传输数据:

@protocol NSURLProtocolClient <NSObject>
...
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;

- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;

- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
...
@end

当然这个协议中还有很多其他的方法,比如 HTTPS 验证、重定向以及响应缓存相关的方法,你需要在合适的时候调用这些代理方法,对信息进行传递。

如果你只是继承了 NSURLProtocol 并且实现了上述方法,依然不能达到预期的效果,完成对 HTTP 请求的拦截,你还需要在 URL 加载系统中注册当前类:

[NSURLProtocol registerClass:self];


需要注意的是 NSURLProtocol 只能拦截 UIURLConnectionNSURLSession 和 UIWebView 中的请求,对于 WKWebView 中发出的网络请求也无能为力,如果真的要拦截来自 WKWebView 中的请求,还是需要实现 WKWebView 对应的 WKNavigationDelegate,并在代理方法中获取请求。 无论是 NSURLProtocolNSURLConnection 还是 NSURLSession 都会走底层的 socket,但是 WKWebView 可能由于基于 WebKit,并不会执行 C socket 相关的函数对 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:200 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-test

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 > 300) && (responseStub.statusCode < 400)) && 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>0) && stubResponse.inputStream.hasBytesAvailable && (!self.stopped)) {
        OHHTTPStubsStreamTimingInfo timingInfo = {
            .slotTime = kSlotTime,
            .cumulativeChunkSize = 0
        };

        if(stubResponse.responseTime < 0) {
            timingInfo.chunkSizePerSlot = (fabs(stubResponse.responseTime) * 1000) * 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 == 0) {
            [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 > 0) {
                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);

这些 C 语言函数能够帮助我们构造 HTTP 响应。

总结

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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值