Introduction
In this blog post I will demonstrate how to mock HTTP requests usingĀ an Objective-C runtime dynamic method replacement technique known as method swizzling. I will show how this can be used in tandem with some unit test technologies to help with development of iOS web-service client side code.
In this example, the actual work of the HTTP request is embodied in my AsyncURLConnection class. It uses NSURLConnection to perform an NSURLRequest in an asynchronous manner, using completion handler blocks to return the response to the caller. I could have chosen to mock the native APIs but mocking at the AsyncURLConnection level provides the desired effect more easily.
Method Swizzling Helper Class
This is a generic helper class for method swizzling (swapping). This gets used in tests as seen below. The
swizzleClassMethod:selector:swizzleClass:
method takes a class and a method selector along with a class with an alternative implementation of the method given by the selector. The class swaps the implementation of the existing class method with the corresponding method in the swizzleClass.
#import <Foundation/Foundation.h> #import <objc/runtime.h> @interface Swizzler : NSObject - (void)swizzleClassMethod:(Class)target_class selector:(SEL)selector swizzleClass:(Class)swizzleClass; - (void)deswizzle; @end #import "Swizzler.h" @implementation Swizzler Method originalMethod = nil; Method swizzleMethod = nil; /** Swaps the implementation of an existing class method with the corresponding method in the swizzleClass. */ - (void)swizzleClassMethod:(Class)targetClass selector:(SEL)selector swizzleClass:(Class)swizzleClass { originalMethod = class_getClassMethod(targetClass, selector); // save the oringinal implementation so it can be restored swizzleMethod = class_getClassMethod(swizzleClass, selector); // save the replacement method method_exchangeImplementations(originalMethod, swizzleMethod); // perform the swap, replacing the original with the swizzle method implementation. } /** Restores the implementation of an existing class method with its original implementation. */ - (void)deswizzle { method_exchangeImplementations(swizzleMethod, originalMethod); // perform the swap, replacing the previously swizzled method with the original implementation. swizzleMethod = nil; originalMethod = nil; } @end
Class to be mocked
This is the interface of a class that connects and loads a URL. I will be replacing the implementation so its details are left out of here for brevity.
#import <Foundation/Foundation.h> typedef void (^completeBlock_t)(NSData *data); typedef void (^errorBlock_t)(NSError *error); @interface AsyncURLConnection : NSObject { NSMutableData *data; completeBlock_t completeBlock; errorBlock_t errorBlock; } /** Asynchronously performs an HTTP GET - invokes one of the blocks depending on the response to the request */ + (id)request:(NSString *)requestUrl completeBlock:(completeBlock_t)aCompleteBlock errorBlock:(errorBlock_t)anErrorBlock; /** Initializes and asynchronously performs an HTTP GET - invokes one of the blocks depending on the response to the request */ - (id)initWithRequest:(NSString *)requestUrl completeBlock:(completeBlock_t)aCompleteBlock errorBlock:(errorBlock_t)anErrorBlock; // If both HTTP Basic authentication optional parameters are non-empty strings, request will encode the URL with them. @property (nonatomic, copy) NSString *authUserName; // HTTP Basic authentication optional parameter @property (nonatomic, copy) NSString *authPassword; // HTTP Basic authentication optional parameter @property (nonatomic, copy) NSString *accept; // Optional parameter to set the response type accepted (default is @"application/json") @end
Example of Usage in a Test
In this test I am testing that I am correctly processing the JSON data returned from the web service. However, I want to isolate the actual web service from the test. I want a test to validate the creation of the NSDictionary response object from the JSON.
As an aside, by removing the Swizzler code, this test could be run against the actual web service; but that is not the intent here.
Here is what the JSON response looks like –
{ "coaches" : [ { "id" : 1, "name" : "Jeff Tucker", "thumbnail_url" : "http://img.breakingmuscle.com/sites/default/files/imagecache/full_portrait/images/author/64337_10150578149548471_1189630488_n.jpg" }, { "id" : 2, "name" : "Traver H. Boehm", "thumbnail_url" : "http://img.breakingmuscle.com/sites/default/files/imagecache/full_portrait/images/author/traver.jpg" }, { "id" :3, "name" : "Jeff Tucker", "thumbnail_url" : "http://img.breakingmuscle.com/sites/default/files/imagecache/full_portrait/images/author/64337_10150578149548471_1189630488_n.jpg" }, { "id" : 4, "name" : "Traver H. Boehm", "thumbnail_url" : "http://img.breakingmuscle.com/sites/default/files/imagecache/full_portrait/images/author/traver.jpg" }, { "id" : 5, "name" : "Jeff Tucker", "thumbnail_url" : "http://img.breakingmuscle.com/sites/default/files/imagecache/full_portrait/images/author/64337_10150578149548471_1189630488_n.jpg" }, { "id" : 6, "name" : "Traver H. Boehm", "thumbnail_url" : "http://img.breakingmuscle.com/sites/default/files/imagecache/full_portrait/images/author/traver.jpg" } ], "per_page": 6, "page": 1, "total" : 11, "total_pages": 2 }
The method under test here is:-
- (void)loadCollectionFromEndpoint:(NSString *)endpoint successBlock:(JSONLoaderLoadSuccessBlock)successBlock failBlock:(JSONLoaderLoadFailBlock)failBlock
This method calls the AsyncURLConnection class method
+ (id)request:(NSString *)requestUrl completeBlock:(completeBlock_t)completeBlock errorBlock:(errorBlock_t)errorBlock;
I will replace the existing implementation of that method with one that does not actually make an HTTP request, but instead gets JSON from a local file.
The unit test uses OCHamcrest matchers and XCode’s built in OCUnit.
- (void)test_loadPopularCoaches_success { // Test set-up Swizzler *swizzler = [Swizzler new]; // Replace the actual AsyncURLConnection method with our own test one. [swizzler swizzleClassMethod:[AsyncURLConnection class] selector:@selector(request:completeBlock:errorBlock:) swizzleClass:[RequestPopularCoachesPassMock class]]; __block BOOL hasCalledBack = NO; // Test NSString *endpoint = [NSString stringWithFormat:@"%@?per_page=6&page=1&popular=true", kCoachesEndpoint]; [[JSONLoader sharedJSONLoader] loadCollectionFromEndpoint:endpoint successBlock:^(NSDictionary *collection) { NSArray *coaches = [collection valueForKey:@"coaches"]; assertThat(coaches, hasCountOf(6)); int i = 0; for (NSDictionary *coach in coaches) { STAssertTrue([coach isKindOfClass:[NSDictionary class]], @"Incorrect class"); assertThat(coach, equalTo([popularCoaches objectAtIndex:i])); // popularCoaches defined elsewhere with expected data ++i; } assertThat([collection valueForKey:@"per_page"], equalToInt(6)); assertThat([collection valueForKey:@"page"], equalToInt(1)); assertThat([collection valueForKey:@"total"], equalToInt(11)); assertThat([collection valueForKey:@"total_pages"], equalToInt(2)); hasCalledBack = YES; } failBlock:^(NSError *error) { NSLog(@"%@", [error localizedDescription]); STFail(@"Unexpected error returned"); hasCalledBack = YES; }]; // see http://drewsmitscode.posterous.com/testing-asynchronous-code-in-objective-c NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:1]; while (hasCalledBack == NO) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:loopUntil]; } swizzler = nil; }
You can use any class to provide an implementation of the replacement method. This approach allows you can return various responses from your HTTP request. Here is one that returns the expected payload that is read from a file.
@interface RequestPopularCoachesPassMock : NSObject + (id)request:(NSString *)requestUrl completeBlock:(completeBlock_t)completeBlock errorBlock:(errorBlock_t)errorBlock; @end @implementation RequestPopularCoachesPassMock // Swizzled method + (id)request:(NSString *)requestUrl completeBlock:(completeBlock_t)completeBlock errorBlock:(errorBlock_t)errorBlock { if (completeBlock) { NSData *data = [FileReader dataWithContentsOfBundleFile:@"popularCoaches.json"]; completeBlock(data); } return self; } @end
Conclusion
In this blog post I have demonstrated how to use the Objective-C runtime dynamic method replacement technique known as *method swizzling* to assist with unit testing in a situation where there is a 3rd party web service that I want to mock. This technique can be used during development to assist with integrating of an iOS app with a web service. It helps speed up development and prototyping. It can also be applied during development in the normal running of your app if you want to mock out portions of or all of a web service.