├── Categories ├── NSString+mock.h ├── NSString+mock.m ├── NSURLRequest+GYURLRequestProtocol.h └── NSURLRequest+GYURLRequestProtocol.m ├── GYHttpMock.h ├── GYHttpMock.m ├── GYHttpMock.podspec ├── GYMatcher.h ├── GYMatcher.m ├── GYMockURLProtocol.h ├── GYMockURLProtocol.m ├── Hooks ├── GYHttpClientHook.h ├── GYHttpClientHook.m ├── GYNSURLConnectionHook.h ├── GYNSURLConnectionHook.m ├── GYNSURLSessionHook.h └── GYNSURLSessionHook.m ├── LICENSE ├── README.md ├── Request ├── GYMockRequest.h ├── GYMockRequest.m ├── GYMockRequestDSL.h └── GYMockRequestDSL.m └── Response ├── GYMockResponse.h ├── GYMockResponse.m ├── GYMockResponseDSL.h └── GYMockResponseDSL.m /Categories/NSString+mock.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+mock.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString(mock) 12 | 13 | - (NSRegularExpression *)regex; 14 | 15 | - (NSData *)data; 16 | @end 17 | -------------------------------------------------------------------------------- /Categories/NSString+mock.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+mock.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "NSString+mock.h" 10 | 11 | @implementation NSString(mock) 12 | 13 | - (NSRegularExpression *)regex { 14 | NSError *error = nil; 15 | NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:self options:0 error:&error]; 16 | if (error) { 17 | [NSException raise:NSInvalidArgumentException format:@"Invalid regex pattern: %@\nError: %@", self, error]; 18 | } 19 | return regex; 20 | } 21 | 22 | - (NSData *)data { 23 | return [self dataUsingEncoding:NSUTF8StringEncoding]; 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Categories/NSURLRequest+GYURLRequestProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSURLRequest+GYURLRequestProtocol.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "GYMockRequest.h" 11 | 12 | @interface NSURLRequest (GYURLRequestProtocol) 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Categories/NSURLRequest+GYURLRequestProtocol.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSURLRequest+GYURLRequestProtocol.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "NSURLRequest+GYURLRequestProtocol.h" 10 | 11 | @implementation NSURLRequest (GYURLRequestProtocol) 12 | 13 | - (NSURL*)url { 14 | return self.URL; 15 | } 16 | 17 | - (NSString *)method { 18 | return self.HTTPMethod; 19 | } 20 | 21 | - (NSDictionary *)headers { 22 | return self.allHTTPHeaderFields; 23 | } 24 | 25 | - (NSData *)body { 26 | if (self.HTTPBodyStream) { 27 | NSInputStream *stream = self.HTTPBodyStream; 28 | NSMutableData *data = [NSMutableData data]; 29 | [stream open]; 30 | size_t bufferSize = 4096; 31 | uint8_t *buffer = malloc(bufferSize); 32 | if (buffer == NULL) { 33 | [NSException raise:@"MallocFailure" format:@"Could not allocate %zu bytes to read HTTPBodyStream", bufferSize]; 34 | } 35 | while ([stream hasBytesAvailable]) { 36 | NSInteger bytesRead = [stream read:buffer maxLength:bufferSize]; 37 | if (bytesRead > 0) { 38 | NSData *readData = [NSData dataWithBytes:buffer length:bytesRead]; 39 | [data appendData:readData]; 40 | } else if (bytesRead < 0) { 41 | [NSException raise:@"StreamReadError" format:@"An error occurred while reading HTTPBodyStream (%ld)", (long)bytesRead]; 42 | } else if (bytesRead == 0) { 43 | break; 44 | } 45 | } 46 | free(buffer); 47 | [stream close]; 48 | 49 | return data; 50 | } 51 | 52 | return self.HTTPBody; 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /GYHttpMock.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYHttpMock.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "GYMockRequest.h" 11 | #import "GYHttpClientHook.h" 12 | #import "GYMockResponse.h" 13 | 14 | #import "GYMockRequestDSL.h" 15 | #import "GYMockResponseDSL.h" 16 | #import "NSString+mock.h" 17 | 18 | @interface GYHttpMock : NSObject 19 | 20 | @property (nonatomic, strong) NSMutableArray *stubbedRequests; 21 | @property (nonatomic, strong) NSMutableArray *hooks; 22 | @property (nonatomic, assign, getter = isStarted) BOOL started; 23 | @property (nonatomic, copy) void (^logBlock)(NSString *logStr); 24 | 25 | + (GYHttpMock *)sharedInstance; 26 | 27 | - (void)startMock; 28 | - (void)stopMock; 29 | 30 | - (GYMockResponse *)responseForRequest:(id)request; 31 | - (void)addMockRequest:(GYMockRequest *)request; 32 | 33 | - (void)log:(NSString *)fmt, ...; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /GYHttpMock.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYHttpMock.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYHttpMock.h" 10 | #import "GYNSURLConnectionHook.h" 11 | #import "GYNSURLSessionHook.h" 12 | #import "GYHttpClientHook.h" 13 | 14 | static GYHttpMock *sharedInstance = nil; 15 | 16 | @implementation GYHttpMock 17 | 18 | + (GYHttpMock *)sharedInstance 19 | { 20 | static dispatch_once_t onceToken; 21 | dispatch_once(&onceToken, ^{ 22 | sharedInstance = [[self alloc] init]; 23 | }); 24 | return sharedInstance; 25 | } 26 | 27 | - (id)init 28 | { 29 | self = [super init]; 30 | if (self) { 31 | _stubbedRequests = [NSMutableArray array]; 32 | _hooks = [NSMutableArray array]; 33 | [self registerHook:[[GYNSURLConnectionHook alloc] init]]; 34 | if (NSClassFromString(@"NSURLSession") != nil) { 35 | [self registerHook:[[GYNSURLSessionHook alloc] init]]; 36 | } 37 | } 38 | return self; 39 | } 40 | 41 | - (void)startMock 42 | { 43 | if (!self.isStarted){ 44 | [self loadHooks]; 45 | self.started = YES; 46 | } 47 | } 48 | 49 | - (void)stopMock 50 | { 51 | [self unloadHooks]; 52 | self.started = NO; 53 | [self clearMocks]; 54 | } 55 | 56 | - (void)clearMocks 57 | { 58 | @synchronized(_stubbedRequests) { 59 | [_stubbedRequests removeAllObjects]; 60 | } 61 | } 62 | 63 | - (void)addMockRequest:(GYMockRequest *)request { 64 | @synchronized(_stubbedRequests) { 65 | [self.stubbedRequests addObject:request]; 66 | } 67 | } 68 | 69 | - (void)registerHook:(GYHttpClientHook *)hook 70 | { 71 | if (![self hookWasRegistered:hook]) { 72 | @synchronized(_hooks) { 73 | [_hooks addObject:hook]; 74 | } 75 | } 76 | } 77 | 78 | - (BOOL)hookWasRegistered:(GYHttpClientHook *)aHook { 79 | @synchronized(_hooks) { 80 | for (GYHttpClientHook *hook in _hooks) { 81 | if ([hook isMemberOfClass: [aHook class]]) { 82 | return YES; 83 | } 84 | } 85 | return NO; 86 | } 87 | } 88 | 89 | - (GYMockResponse *)responseForRequest:(id)request 90 | { 91 | @synchronized(_stubbedRequests) { 92 | 93 | for(GYMockRequest *someStubbedRequest in _stubbedRequests) { 94 | if ([someStubbedRequest matchesRequest:request]) { 95 | someStubbedRequest.response.isUpdatePartResponseBody = someStubbedRequest.isUpdatePartResponseBody; 96 | return someStubbedRequest.response; 97 | } 98 | } 99 | 100 | return nil; 101 | } 102 | 103 | } 104 | 105 | - (void)loadHooks { 106 | @synchronized(_hooks) { 107 | for (GYHttpClientHook *hook in _hooks) { 108 | [hook load]; 109 | } 110 | } 111 | } 112 | 113 | - (void)unloadHooks { 114 | @synchronized(_hooks) { 115 | for (GYHttpClientHook *hook in _hooks) { 116 | [hook unload]; 117 | } 118 | } 119 | } 120 | 121 | - (void)log:(NSString *)fmt, ... { 122 | if (!fmt || !self.logBlock) { 123 | return; 124 | } 125 | 126 | NSString *msg = nil; 127 | va_list args; 128 | va_start(args, fmt); 129 | msg = [[NSString alloc] initWithFormat:fmt arguments:args]; 130 | va_end(args); 131 | 132 | self.logBlock(msg); 133 | } 134 | 135 | @end 136 | -------------------------------------------------------------------------------- /GYHttpMock.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "GYHttpMock" 3 | s.version = "1.0.0" 4 | s.summary = "Library for replace part/all HTTP response based Nocilla." 5 | 6 | s.homepage = "https://github.com/hypoyao/GYHttpMock" 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { "hypoyao" => "hypoyao@qq.com" } 9 | s.source = { :git => "https://github.com/hypoyao/GYHttpMock.git", :tag => "1.0.0" } 10 | s.platform = :ios, '7.0' 11 | s.requires_arc = true 12 | s.source_files = '**/*.{h,m}' 13 | s.public_header_files = [ 14 | 'GYHttpMock.h', 15 | 'GYMatcher.h', 16 | 'GYNSURLProtocol.h', 17 | 'Categories/NSString+mock.h', 18 | 'Categories/NSURLRequest+GYURLRequestProtocol.h', 19 | 'Hooks/GYHttpClientHook.h', 20 | 'Hooks/GYURLHook.h', 21 | 'Hooks/GYNSURLSessionHook.h', 22 | 'Request/GYSubRequest.h', 23 | 'Request/GYMockRequest.h', 24 | 'Request/GYMockRequestDSL.h', 25 | 'Response/GYMockResponse.h', 26 | 'Response/GYMockResponseDSL.h', 27 | ] 28 | 29 | s.frameworks = 'Foundation','CFNetwork' 30 | 31 | end 32 | 33 | 34 | -------------------------------------------------------------------------------- /GYMatcher.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYMatcher.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef NS_OPTIONS(NSUInteger, GYMatcherType) { 12 | GYMatcherTypeString, 13 | GYMatcherTypeData, 14 | GYMatcherTypeRegex 15 | }; 16 | 17 | @interface GYMatcher : NSObject 18 | 19 | @property (nonatomic, assign) GYMatcherType matchType; 20 | @property (nonatomic, copy, readonly) NSString *string; 21 | @property (nonatomic, strong, readonly) NSData *data; 22 | @property (nonatomic, strong, readonly) NSRegularExpression *regex; 23 | 24 | + (instancetype)GYMatcherWithObject:(id)object; 25 | 26 | - (instancetype)initWithString:(NSString *)string; 27 | - (instancetype)initWithData:(NSData *)data; 28 | - (instancetype)initWithRegex:(NSRegularExpression *)regex; 29 | 30 | - (BOOL)match:(GYMatcher *)matcher; 31 | @end 32 | -------------------------------------------------------------------------------- /GYMatcher.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYMatcher.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYMatcher.h" 10 | 11 | @implementation GYMatcher 12 | 13 | + (instancetype)GYMatcherWithObject:(id)object 14 | { 15 | if ([object isKindOfClass:[NSString class]]) { 16 | return [[GYMatcher alloc] initWithString:object]; 17 | } else if ([object isKindOfClass:[NSData class]]) { 18 | return [[GYMatcher alloc] initWithData:object]; 19 | } else if ([object isKindOfClass:[NSRegularExpression class]]) { 20 | return [[GYMatcher alloc] initWithRegex:object]; 21 | } else { 22 | return nil; 23 | } 24 | } 25 | 26 | - (instancetype)initWithString:(NSString *)string 27 | { 28 | if (self = [super init]) { 29 | _string = string; 30 | _matchType = GYMatcherTypeString; 31 | } 32 | return self; 33 | } 34 | 35 | - (instancetype)initWithData:(NSData *)data 36 | { 37 | if (self = [super init]) { 38 | _data = data; 39 | _matchType = GYMatcherTypeData; 40 | } 41 | return self; 42 | } 43 | - (instancetype)initWithRegex:(NSRegularExpression *)regex 44 | { 45 | if (self = [super init]) { 46 | _regex = regex; 47 | _matchType = GYMatcherTypeRegex; 48 | } 49 | return self; 50 | } 51 | 52 | - (BOOL)match:(GYMatcher *)matcher 53 | { 54 | switch (self.matchType) { 55 | case GYMatcherTypeString: 56 | return [_string isEqualToString:matcher.string]; 57 | case GYMatcherTypeData: 58 | return [_data isEqual:matcher.data]; 59 | case GYMatcherTypeRegex: 60 | return [_regex numberOfMatchesInString:matcher.string options:0 range:NSMakeRange(0, matcher.string.length)] > 0; 61 | default: 62 | return NO; 63 | } 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /GYMockURLProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYNSURLProtocol.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GYMockURLProtocol : NSURLProtocol 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /GYMockURLProtocol.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYNSURLProtocol.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYMockURLProtocol.h" 10 | #import "GYHttpMock.h" 11 | #import "GYMockResponse.h" 12 | #import "GYHttpMock.h" 13 | 14 | @interface NSHTTPURLResponse(UndocumentedInitializer) 15 | - (id)initWithURL:(NSURL*)URL statusCode:(NSInteger)statusCode headerFields:(NSDictionary*)headerFields requestTime:(double)requestTime; 16 | @end 17 | 18 | @implementation GYMockURLProtocol 19 | + (BOOL)canInitWithRequest:(NSURLRequest *)request { 20 | [[GYHttpMock sharedInstance] log:@"mock request: %@", request]; 21 | 22 | GYMockResponse* stubbedResponse = [[GYHttpMock sharedInstance] responseForRequest:(id)request]; 23 | if (stubbedResponse && !stubbedResponse.shouldNotMockAgain) { 24 | return YES; 25 | } 26 | return NO; 27 | } 28 | 29 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { 30 | return request; 31 | } 32 | 33 | + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { 34 | return NO; 35 | } 36 | 37 | - (void)startLoading { 38 | NSURLRequest* request = [self request]; 39 | id client = [self client]; 40 | 41 | GYMockResponse* stubbedResponse = [[GYHttpMock sharedInstance] responseForRequest:(id)request]; 42 | 43 | if (stubbedResponse.shouldFail) { 44 | [client URLProtocol:self didFailWithError:stubbedResponse.error]; 45 | } 46 | else if (stubbedResponse.isUpdatePartResponseBody) { 47 | stubbedResponse.shouldNotMockAgain = YES; 48 | NSOperationQueue *queue = [[NSOperationQueue alloc]init]; 49 | [NSURLConnection sendAsynchronousRequest:request 50 | queue:queue 51 | completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){ 52 | if (error) { 53 | NSLog(@"Httperror:%@%@", error.localizedDescription,@(error.code)); 54 | [client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]]; 55 | }else{ 56 | 57 | id json = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; 58 | NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:json]; 59 | if (!error && json) { 60 | NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:stubbedResponse.body options:NSJSONReadingMutableContainers error:nil]; 61 | 62 | [self addEntriesFromDictionary:dict to:result]; 63 | } 64 | 65 | NSData *combinedData = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil]; 66 | 67 | 68 | [client URLProtocol:self didReceiveResponse:response 69 | cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 70 | [client URLProtocol:self didLoadData:combinedData]; 71 | [client URLProtocolDidFinishLoading:self]; 72 | } 73 | stubbedResponse.shouldNotMockAgain = NO; 74 | }]; 75 | 76 | } 77 | else { 78 | NSHTTPURLResponse* urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:stubbedResponse.statusCode HTTPVersion:@"1.1" headerFields:stubbedResponse.headers]; 79 | 80 | if (stubbedResponse.statusCode < 300 || stubbedResponse.statusCode > 399 81 | || stubbedResponse.statusCode == 304 || stubbedResponse.statusCode == 305 ) { 82 | NSData *body = stubbedResponse.body; 83 | 84 | [client URLProtocol:self didReceiveResponse:urlResponse 85 | cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 86 | [client URLProtocol:self didLoadData:body]; 87 | [client URLProtocolDidFinishLoading:self]; 88 | } else { 89 | NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; 90 | [cookieStorage setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:stubbedResponse.headers forURL:request.URL] 91 | forURL:request.URL mainDocumentURL:request.URL]; 92 | 93 | NSURL *newURL = [NSURL URLWithString:[stubbedResponse.headers objectForKey:@"Location"] relativeToURL:request.URL]; 94 | NSMutableURLRequest *redirectRequest = [NSMutableURLRequest requestWithURL:newURL]; 95 | 96 | [redirectRequest setAllHTTPHeaderFields:[NSHTTPCookie requestHeaderFieldsWithCookies:[cookieStorage cookiesForURL:newURL]]]; 97 | 98 | [client URLProtocol:self 99 | wasRedirectedToRequest:redirectRequest 100 | redirectResponse:urlResponse]; 101 | // According to: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Listings/CustomHTTPProtocol_Core_Code_CustomHTTPProtocol_m.html 102 | // needs to abort the original request 103 | [client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]]; 104 | 105 | } 106 | } 107 | } 108 | 109 | - (void)stopLoading { 110 | } 111 | 112 | - (void)addEntriesFromDictionary:(NSDictionary *)dict to:(NSMutableDictionary *)targetDict 113 | { 114 | for (NSString *key in dict) { 115 | if (!targetDict[key] || [dict[key] isKindOfClass:[NSString class]] || [dict[key] isKindOfClass:[NSNumber class]]) { 116 | [targetDict addEntriesFromDictionary:dict]; 117 | } else if ([dict[key] isKindOfClass:[NSArray class]]) { 118 | NSMutableArray *mutableArray = [NSMutableArray array]; 119 | for (NSDictionary *targetArrayDict in targetDict[key]) { 120 | NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:targetArrayDict]; 121 | for (NSDictionary *arrayDict in dict[key]) { 122 | [self addEntriesFromDictionary:arrayDict to:mutableDict]; 123 | } 124 | [mutableArray addObject:mutableDict]; 125 | } 126 | [targetDict setObject:mutableArray forKey:key]; 127 | } else if ([dict[key] isKindOfClass:[NSDictionary class]]) { 128 | NSMutableDictionary *mutableDict = [NSMutableDictionary dictionaryWithDictionary:targetDict[key]]; 129 | [self addEntriesFromDictionary:dict[key] to:mutableDict]; 130 | [targetDict setObject:mutableDict forKey:key]; 131 | } 132 | } 133 | } 134 | 135 | @end 136 | -------------------------------------------------------------------------------- /Hooks/GYHttpClientHook.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYHttpClientHook.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GYHttpClientHook : NSObject 12 | 13 | - (void)load; 14 | - (void)unload; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Hooks/GYHttpClientHook.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYHttpClientHook.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYHttpClientHook.h" 10 | 11 | @implementation GYHttpClientHook 12 | 13 | - (void)load { 14 | [NSException raise:NSInternalInconsistencyException 15 | format:@"Method '%@' not implemented. Subclass '%@' and override it", NSStringFromSelector(_cmd), NSStringFromClass([self class])]; 16 | } 17 | 18 | - (void)unload { 19 | [NSException raise:NSInternalInconsistencyException 20 | format:@"Method '%@' not implemented. Subclass '%@' and override it", NSStringFromSelector(_cmd), NSStringFromClass([self class])]; 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Hooks/GYNSURLConnectionHook.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYURLHook.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "GYHttpClientHook.h" 11 | 12 | @interface GYNSURLConnectionHook : GYHttpClientHook 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Hooks/GYNSURLConnectionHook.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYURLHook.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYNSURLConnectionHook.h" 10 | #import "GYMockURLProtocol.h" 11 | 12 | @implementation GYNSURLConnectionHook 13 | 14 | - (void)load { 15 | [NSURLProtocol registerClass:[GYMockURLProtocol class]]; 16 | } 17 | 18 | - (void)unload { 19 | [NSURLProtocol unregisterClass:[GYMockURLProtocol class]]; 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Hooks/GYNSURLSessionHook.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYNSURLSessionHook.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "GYHttpClientHook.h" 11 | 12 | @interface GYNSURLSessionHook : GYHttpClientHook 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Hooks/GYNSURLSessionHook.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYNSURLSessionHook.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYNSURLSessionHook.h" 10 | #import "GYMockURLProtocol.h" 11 | #import 12 | 13 | @implementation GYNSURLSessionHook 14 | - (void)load { 15 | Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration"); 16 | [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]]; 17 | } 18 | 19 | - (void)unload { 20 | Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration"); 21 | [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]]; 22 | } 23 | 24 | - (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub { 25 | 26 | Method originalMethod = class_getInstanceMethod(original, selector); 27 | Method stubMethod = class_getInstanceMethod(stub, selector); 28 | if (!originalMethod || !stubMethod) { 29 | [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NSURLSession hook."]; 30 | } 31 | method_exchangeImplementations(originalMethod, stubMethod); 32 | } 33 | 34 | - (NSArray *)protocolClasses { 35 | return @[[GYMockURLProtocol class]]; 36 | } 37 | @end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 hypoyao 2 | MIT License 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GYHttpMock 2 | Library for replacing part/all HTTP response based on Nocilla. 3 | 4 | ## Features 5 | * Support NSURLConnection, NSURLSession. 6 | * Support replacing totally or partly HTTP response 7 | * Match requests with regular expressions. 8 | * Support json file for response 9 | 10 | ## Installation with CocoaPods 11 | 12 | To integrate GYHttpMock into your Xcode project using CocoaPods, specify it in your `Podfile`: 13 | 14 | ```ruby 15 | pod 'GYHttpMock' 16 | ``` 17 | 18 | Then, run the following command: 19 | 20 | ```bash 21 | $ pod install 22 | ``` 23 | 24 | ## Usage 25 | mocking a request before the real request anywhere. 26 | 27 | #### Mock a simple request 28 | It will return the default response, which is a 200 and an empty body. 29 | 30 | ```objc 31 | mockRequest(@"GET", @"http://www.google.com"); 32 | ``` 33 | 34 | #### Mock requests with regular expressions 35 | ```objc 36 | mockRequest(@"GET", @"(.*?)google.com(.*?)".regex); 37 | ``` 38 | 39 | 40 | #### Mock a request with updating response partly 41 | 42 | ```objc 43 | mockRequest(@"POST", @"http://www.google.com"). 44 | isUpdatePartResponseBody(YES). 45 | withBody(@"{\"name\":\"abc\"}".regex); 46 | andReturn(200). 47 | withBody(@"{\"key\":\"value\"}"); 48 | ``` 49 | 50 | #### Mock a request with json file response 51 | 52 | ```objc 53 | mockRequest(@"POST", @"http://www.google.com"). 54 | isUpdatePartResponseBody(YES). 55 | withBody(@"{\"name\":\"abc\"}".regex); 56 | andReturn(200). 57 | withBody(@"google.json"); 58 | ``` 59 | ##### Examle for update part response 60 | orginal response: 61 | {"data":{"id":"abc","location":"GZ"}} 62 | 63 | updatedBody: google.json 64 | {"data":{"time":"today"}} 65 | 66 | final resoponse: 67 | {"data":{"id":"abc","location":"GZ","time":"today"}} 68 | 69 | #### All together 70 | ```objc 71 | mockRequest(@"POST", @"http://www.google.com"). 72 | withHeaders(@{@"Accept": @"application/json"}). 73 | withBody(@"\"name\":\"foo\"".regex). 74 | isUpdatePartResponseBody(YES). 75 | andReturn(200). 76 | withHeaders(@{@"Content-Type": @"application/json"}). 77 | withBody(@"google.json"); 78 | ``` 79 | #### Add log 80 | ```objc 81 | [GYHttpMock sharedInstance].logBlock = ^(NSString *logStr) { 82 | NSLOG(@"%@", logStr); 83 | }; 84 | ``` 85 | 86 | ## License 87 | 88 | GYHttpMock is released under the MIT license. See LICENSE for details. 89 | -------------------------------------------------------------------------------- /Request/GYMockRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYSubRequest.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class GYMockResponse; 12 | @class GYMatcher; 13 | 14 | @protocol GYHTTPRequest 15 | 16 | @property (nonatomic, strong, readonly) NSURL *url; 17 | @property (nonatomic, strong, readonly) NSString *method; 18 | @property (nonatomic, strong, readonly) NSDictionary *headers; 19 | @property (nonatomic, strong, readonly) NSData *body; 20 | 21 | @end 22 | 23 | @interface GYMockRequest : NSObject 24 | @property (nonatomic, strong) NSString *method; 25 | @property (nonatomic, strong) GYMatcher *urlMatcher; 26 | @property (nonatomic, strong) NSDictionary *headers; 27 | @property (nonatomic, strong, readwrite) GYMatcher *body; 28 | @property (nonatomic, assign, readwrite) BOOL isUpdatePartResponseBody; 29 | 30 | @property (nonatomic, strong) GYMockResponse *response; 31 | 32 | - (instancetype)initWithMethod:(NSString *)method url:(NSString *)url; 33 | - (instancetype)initWithMethod:(NSString *)method urlMatcher:(GYMatcher *)urlMatcher; 34 | 35 | - (void)setHeader:(NSString *)header value:(NSString *)value; 36 | 37 | - (BOOL)matchesRequest:(id)request; 38 | @end 39 | -------------------------------------------------------------------------------- /Request/GYMockRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYSubRequest.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYMockRequest.h" 10 | #import "GYMatcher.h" 11 | #import "GYMockResponse.h" 12 | 13 | @implementation GYMockRequest 14 | 15 | - (instancetype)initWithMethod:(NSString *)method url:(NSString *)url { 16 | return [self initWithMethod:method urlMatcher:[[GYMatcher alloc] initWithString:url]]; 17 | } 18 | 19 | - (instancetype)initWithMethod:(NSString *)method urlMatcher:(GYMatcher *)urlMatcher; { 20 | self = [super init]; 21 | if (self) { 22 | self.method = method; 23 | self.urlMatcher = urlMatcher; 24 | self.headers = [NSMutableDictionary dictionary]; 25 | } 26 | return self; 27 | } 28 | 29 | - (void)setHeader:(NSString *)header value:(NSString *)value { 30 | [self.headers setValue:value forKey:header]; 31 | } 32 | 33 | - (NSString *)description { 34 | return [NSString stringWithFormat:@"StubRequest:\nMethod: %@\nURL: %@\nHeaders: %@\nBody: %@\nResponse: %@", 35 | self.method, 36 | self.urlMatcher, 37 | self.headers, 38 | self.body, 39 | self.response]; 40 | } 41 | 42 | - (GYMockResponse *)response { 43 | if (!_response) { 44 | _response = [[GYMockResponse alloc] initDefaultResponse]; 45 | } 46 | return _response; 47 | } 48 | 49 | - (BOOL)matchesRequest:(id)request { 50 | if ([self matchesMethod:request] 51 | && [self matchesURL:request] 52 | && [self matchesHeaders:request] 53 | && [self matchesBody:request] 54 | ) { 55 | return YES; 56 | } 57 | return NO; 58 | } 59 | 60 | - (BOOL)matchesMethod:(id)request { 61 | if (!self.method || [self.method isEqualToString:request.method]) { 62 | return YES; 63 | } 64 | return NO; 65 | } 66 | 67 | - (BOOL)matchesURL:(id)request { 68 | GYMatcher *matcher = [[GYMatcher alloc] initWithString:[request.url absoluteString]]; 69 | BOOL result = [self.urlMatcher match:matcher]; 70 | return result; 71 | } 72 | 73 | - (BOOL)matchesHeaders:(id)request { 74 | for (NSString *header in self.headers) { 75 | if (![[request.headers objectForKey:header] isEqualToString:[self.headers objectForKey:header]]) { 76 | return NO; 77 | } 78 | } 79 | return YES; 80 | } 81 | 82 | - (BOOL)matchesBody:(id)request { 83 | NSData *reqBody = request.body; 84 | if (!reqBody) { 85 | return YES; 86 | } 87 | NSString *reqBodyString = [[NSString alloc] initWithData:reqBody encoding:NSUTF8StringEncoding]; 88 | reqBodyString = [reqBodyString stringByReplacingOccurrencesOfString:@" " withString:@""]; 89 | NSAssert(reqBodyString, @"request body is nil"); 90 | 91 | GYMatcher *matcher = [[GYMatcher alloc] initWithString:reqBodyString]; 92 | if (!self.body || [self.body match:matcher]) { 93 | return YES; 94 | } 95 | return NO; 96 | } 97 | @end 98 | -------------------------------------------------------------------------------- /Request/GYMockRequestDSL.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYMockRequestDSL.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | @class GYMockRequestDSL; 11 | @class GYMockResponseDSL; 12 | @class GYMockRequest; 13 | 14 | @protocol GYHTTPBody; 15 | 16 | typedef GYMockRequestDSL *(^WithHeaderMethod)(NSString *, NSString *); 17 | typedef GYMockRequestDSL *(^WithHeadersMethod)(NSDictionary *); 18 | typedef GYMockRequestDSL *(^isUpdatePartResponseBody)(BOOL); 19 | typedef GYMockRequestDSL *(^AndBodyMethod)(id); 20 | typedef GYMockResponseDSL *(^AndReturnMethod)(NSInteger); 21 | typedef void (^AndFailWithErrorMethod)(NSError *error); 22 | 23 | @interface GYMockRequestDSL : NSObject 24 | - (id)initWithRequest:(GYMockRequest *)request; 25 | 26 | @property (nonatomic, strong) GYMockRequest *request; 27 | 28 | @property (nonatomic, strong, readonly) WithHeaderMethod withHeader; 29 | @property (nonatomic, strong, readonly) WithHeadersMethod withHeaders; 30 | @property (nonatomic, strong, readonly) isUpdatePartResponseBody isUpdatePartResponseBody; 31 | @property (nonatomic, strong, readonly) AndBodyMethod withBody; 32 | @property (nonatomic, strong, readonly) AndReturnMethod andReturn; 33 | @property (nonatomic, strong, readonly) AndFailWithErrorMethod andFailWithError; 34 | 35 | 36 | @end 37 | 38 | #ifdef __cplusplus 39 | extern "C" { 40 | #endif 41 | 42 | GYMockRequestDSL * mockRequest(NSString *method, id url); 43 | 44 | #ifdef __cplusplus 45 | } 46 | #endif -------------------------------------------------------------------------------- /Request/GYMockRequestDSL.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYMockRequestDSL.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYMockRequestDSL.h" 10 | #import "GYMockRequest.h" 11 | #import "GYMockResponse.h" 12 | #import "GYMatcher.h" 13 | #import "GYMockResponseDSL.h" 14 | #import "GYHttpMock.h" 15 | 16 | @implementation GYMockRequestDSL 17 | 18 | - (id)initWithRequest:(GYMockRequest *)request { 19 | self = [super init]; 20 | if (self) { 21 | _request = request; 22 | } 23 | return self; 24 | } 25 | - (WithHeadersMethod)withHeaders { 26 | return ^(NSDictionary *headers) { 27 | for (NSString *header in headers) { 28 | NSString *value = [headers objectForKey:header]; 29 | [self.request setHeader:header value:value]; 30 | } 31 | return self; 32 | }; 33 | } 34 | 35 | - (WithHeaderMethod)withHeader { 36 | return ^(NSString * header, NSString * value) { 37 | [self.request setHeader:header value:value]; 38 | return self; 39 | }; 40 | } 41 | 42 | - (isUpdatePartResponseBody)isUpdatePartResponseBody { 43 | return ^(BOOL isUpdate) { 44 | self.request.isUpdatePartResponseBody = isUpdate; 45 | return self; 46 | }; 47 | } 48 | 49 | - (AndBodyMethod)withBody { 50 | return ^(id body) { 51 | self.request.body = [GYMatcher GYMatcherWithObject:body]; 52 | return self; 53 | }; 54 | } 55 | 56 | - (AndReturnMethod)andReturn { 57 | return ^(NSInteger statusCode) { 58 | self.request.response = [[GYMockResponse alloc] initWithStatusCode:statusCode]; 59 | GYMockResponseDSL *responseDSL = [[GYMockResponseDSL alloc] initWithResponse:self.request.response]; 60 | return responseDSL; 61 | }; 62 | } 63 | 64 | 65 | - (AndFailWithErrorMethod)andFailWithError { 66 | return ^(NSError *error) { 67 | self.request.response = [[GYMockResponse alloc] initWithError:error]; 68 | }; 69 | } 70 | 71 | 72 | 73 | @end 74 | 75 | GYMockRequestDSL *mockRequest(NSString *method, id url) { 76 | GYMockRequest *request = [[GYMockRequest alloc] initWithMethod:method urlMatcher:[GYMatcher GYMatcherWithObject:url]]; 77 | GYMockRequestDSL *dsl = [[GYMockRequestDSL alloc] initWithRequest:request]; 78 | [[GYHttpMock sharedInstance] addMockRequest:request]; 79 | [[GYHttpMock sharedInstance] startMock]; 80 | return dsl; 81 | } -------------------------------------------------------------------------------- /Response/GYMockResponse.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYMockResponse.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface GYMockResponse : NSObject 12 | 13 | @property (nonatomic, assign) NSInteger statusCode; 14 | @property (nonatomic, strong) NSData *body; 15 | @property (nonatomic, strong) NSMutableDictionary *headers; 16 | 17 | @property (nonatomic, assign) BOOL shouldFail; 18 | @property (nonatomic, strong) NSError *error; 19 | 20 | @property (nonatomic, assign) BOOL isUpdatePartResponseBody; 21 | 22 | @property (nonatomic, assign) BOOL shouldNotMockAgain; 23 | 24 | - (id)initWithError:(NSError *)error; 25 | - (id)initWithStatusCode:(NSInteger)statusCode; 26 | - (id)initDefaultResponse; 27 | - (void)setHeader:(NSString *)header value:(NSString *)value; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Response/GYMockResponse.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYMockResponse.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYMockResponse.h" 10 | 11 | @implementation GYMockResponse 12 | #pragma Initializers 13 | - (id)initDefaultResponse { 14 | self = [super init]; 15 | if (self) { 16 | self.shouldFail = NO; 17 | 18 | self.statusCode = 200; 19 | self.headers = [NSMutableDictionary dictionary]; 20 | [self.headers addEntriesFromDictionary:@{@"Content-Type":@"application/json;charset=utf-8"}]; 21 | self.body = [@"" dataUsingEncoding:NSUTF8StringEncoding]; 22 | } 23 | return self; 24 | } 25 | 26 | 27 | - (id)initWithError:(NSError *)error { 28 | self = [super init]; 29 | if (self) { 30 | self.shouldFail = YES; 31 | self.error = error; 32 | } 33 | return self; 34 | } 35 | 36 | -(id)initWithStatusCode:(NSInteger)statusCode { 37 | self = [self initDefaultResponse]; 38 | if (self) { 39 | self.statusCode = statusCode; 40 | } 41 | return self; 42 | } 43 | 44 | - (void)setHeader:(NSString *)header value:(NSString *)value { 45 | [self.headers setValue:value forKey:header]; 46 | } 47 | 48 | 49 | - (NSString *)description { 50 | return [NSString stringWithFormat:@"StubRequest:\nStatus Code: %ld\nHeaders: %@\nBody: %@", 51 | (long)self.statusCode, 52 | self.headers, 53 | self.body]; 54 | } 55 | @end 56 | -------------------------------------------------------------------------------- /Response/GYMockResponseDSL.h: -------------------------------------------------------------------------------- 1 | // 2 | // GYMockResponseDSL.h 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class GYMockResponse; 12 | @class GYMockResponseDSL; 13 | 14 | @protocol GYHTTPBody; 15 | 16 | typedef GYMockResponseDSL *(^ResponseWithBodyMethod)(id); 17 | typedef GYMockResponseDSL *(^ResponseWithHeaderMethod)(NSString *, NSString *); 18 | typedef GYMockResponseDSL *(^ResponseWithHeadersMethod)(NSDictionary *); 19 | 20 | @interface GYMockResponseDSL : NSObject 21 | - (id)initWithResponse:(GYMockResponse *)response; 22 | 23 | @property (nonatomic, strong) GYMockResponse *response; 24 | 25 | @property (nonatomic, strong, readonly) ResponseWithHeaderMethod withHeader; 26 | @property (nonatomic, strong, readonly) ResponseWithHeadersMethod withHeaders; 27 | @property (nonatomic, strong, readonly) ResponseWithBodyMethod withBody; 28 | 29 | @end -------------------------------------------------------------------------------- /Response/GYMockResponseDSL.m: -------------------------------------------------------------------------------- 1 | // 2 | // GYMockResponseDSL.m 3 | // GYNetwork 4 | // 5 | // Created by hypo on 16/1/13. 6 | // Copyright © 2016年 hypoyao. All rights reserved. 7 | // 8 | 9 | #import "GYMockResponseDSL.h" 10 | #import "GYMockResponse.h" 11 | #import "NSString+mock.h" 12 | 13 | @implementation GYMockResponseDSL 14 | 15 | - (id)initWithResponse:(GYMockResponse *)response { 16 | self = [super init]; 17 | if (self) { 18 | _response = response; 19 | } 20 | return self; 21 | } 22 | - (ResponseWithHeaderMethod)withHeader { 23 | return ^(NSString * header, NSString * value) { 24 | [self.response setHeader:header value:value]; 25 | return self; 26 | }; 27 | } 28 | 29 | - (ResponseWithHeadersMethod)withHeaders; { 30 | return ^(NSDictionary *headers) { 31 | for (NSString *header in headers) { 32 | NSString *value = [headers objectForKey:header]; 33 | [self.response setHeader:header value:value]; 34 | } 35 | return self; 36 | }; 37 | } 38 | 39 | - (ResponseWithBodyMethod)withBody { 40 | return ^(NSString *body) { 41 | NSString *bodyString = body; 42 | 43 | if ([body hasSuffix:@".json"]) { 44 | NSString *name = [body substringToIndex:body.length-5]; 45 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 46 | NSString *path = [bundle pathForResource:name ofType:@"json"]; 47 | NSAssert(path.length, @"file:%@ not exist",body); 48 | bodyString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; 49 | } 50 | 51 | self.response.body = [bodyString data]; 52 | 53 | //校验 54 | NSError *error = nil; 55 | id json = [NSJSONSerialization JSONObjectWithData:self.response.body options:NSJSONReadingMutableContainers error:&error]; 56 | if (!json) { 57 | NSAssert(json, @"response string is invaild json"); 58 | } 59 | return self; 60 | }; 61 | } 62 | 63 | @end 64 | --------------------------------------------------------------------------------