Using Method-Swizzling to help with Test Driven Development
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 ClassThis 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.
#importThis 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.
#importIn 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 –
The method under test here is:-
- (void)loadCollectionFromEndpoint:(NSString *)endpoint successBlock:(JSONLoaderLoadSuccessBlock)successBlock failBlock:(JSONLoaderLoadFailBlock)failBlockThis 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; } @endConclusionIn 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.
buy cialis
cialis
buy cialis
::: http://www.icodeblog.com/2012/08/08/using-method-swizzling-to-help-with-test-driven-development/ :::
Ditulis oleh Unknown
Rating Blog 5 dari 5
0 komentar:
Posting Komentar