Faking network calls for iOS unit tests
If you have used XCode’s built in unit testing frame work , then you’ve likely hit one of the frustrating points of trying to test your application when making api calls over a network. (I’ll save you some time, and let you know the test dies before the call can finish). That’s not great, but it does not mean you can’t test your application and it’s ability to hit your api.
I’ve seen some creative ways to work around this which essentially block the main thread for a SenTestCase while your web service object does it’s job. However, that can prove to be problematic as one can not guarantee that the network will actually be up, or worse, the back end is not entirely ready for production. This of course is cause for unexpected results.
A clean solution I found was using OCMock. There are a lot of examples and tutorials about how to create OCMock objects and then use them in a unit test framework. However, translating these simple examples to real world practice was less abundant. Today I am sharing a source example I created to demonstrate a real world working application. The unit tests demonstrate a few ways to mock a network call, fake some responses using both blocks and protocol methods. Our application uses the panoramio api to fetch a list of images, and then lets a user pull down a larger version of a selected image.
Let’s start with the simpler test: testMockApiQuerySuccess, which demonstrates a mock api call, and stubbed results – here is the approach:
- create a mock object that acts like our ApiUtility class
- tell the mock object we will stub data jsonForLocation:response:error: method, and when it is called, we will “do” and invocation where we jump in with some stubbed data
- now that we’ve instructed our mock object how to act, let’s do the jsonForLocation:response:error passing in anything we want – we don’t care becuase we will be faking the results in the invocation block
- (void)testMockApiQuerySuccess { //1 id mock = [OCMockObject mockForClass:[ApiUtility class]]; //2 [[[mock stub]andDo:^(NSInvocation *invocation) { //our stubbed results NSArray *photos = [self stubResults]; //the block we will invoke void (^responseHandler)(NSArray *photoArray)= nil; //0 and 1 are reserved for invocation object, ....therefor 2 would be jsonForLocation, 3 is response (block) [invocation getArgument:&responseHandler atIndex:3]; //invoke the block responseHandler(photos); }] jsonForLocation:[OCMArg any] response:[OCMArg any]error:[OCMArg any]]; //3 [mock jsonForLocation:[OCMArg any] response:^(NSArray *photoObjects) { //NSLog(@"calling response block from test %@",photoObjects); STAssertTrue([photoObjects count]==20, @"has results"); } error:^(NSError *error) { STAssertTrue(error == nil, @"should not have an error"); }]; }
Ok, so what about protocols? A common scenario is hitting the network for an image or some data, and implementing a protocol when we have a result/failure. The example is a bit more involved, but still the same premise. As a bonus, we can use some real objects from our application, and just have OCMock fill in a few gaps where we need to mock some behavior (ie: downloading an image from the network):
- create our real objects
- create a mock delegate that will do an invocation when apiUtility:didCompleteDownloadingImage:forPhoto: is called
- here we can mock the network results with a mock UIImage and mock Photo object.
- mock the ApiUtility, and call the protocol method when we fake downloadLargeImage
- do the test- you may see this [OCMArg any], in many of the tests. Because we are mocking the tests, we don’t really need to create and populate a Photo object…it’s just an object place holder
- verify is a useful method in OCMock that will cause our test to fail if the expected methods on our mock objects never get called. In this case, we want to be sure that the delegate method is indeed being called.
-(void)testImageDownloadSuccess { //********************************************************** //1.set up the mock players //********************************************************** __block ApiUtility *util = [[ApiUtility alloc] init]; ViewController *vc = [[ViewController alloc ] init]; //set the delegate and do the action [util setDelegate:vc]; //********************************************************** //2.mock a sceneraio and stub some fake data //********************************************************** id mockDelegate = [OCMockObject partialMockForObject:vc]; [[[mockDelegate expect] andDo:^(NSInvocation *invocation) { //3. //mocks we will pass back id mockImage = [OCMockObject mockForClass:[UIImage class]]; id mockPhoto = [OCMockObject mockForClass:[Photo class]]; // we need to set these properties (to anything), // because ViewController will try to use them on success of image download [[[mockPhoto stub]andReturn:[OCMArg any]] photoId]; [[[mockPhoto stub]andReturn:[OCMArg any]] photoTitle]; [[[mockPhoto stub]andReturn:[OCMArg any]] ownerName]; [invocation setTarget:vc]; [invocation setSelector:@selector(apiUtility:didCompleteDownloadingImage:forPhoto:)]; //again 0 and 1 are reserved for the invocation object , so... [invocation setArgument:&util atIndex:2];//apitutil [invocation setArgument:&mockImage atIndex:3];//fake returned image [invocation setArgument:&mockPhoto atIndex:4];//the mock photo [invocation invoke];//actually involke apiUtility:didCompleteDownloadingImage:forPhoto: }] apiUtility:[OCMArg any] didCompleteDownloadingImage:[OCMArg any] forPhoto:[OCMArg any]]; //4. the mock object that will mock a download/ApiUtility id mockUtil = [OCMockObject partialMockForObject:util]; [[[mockUtil stub] andCall:@selector(apiUtility:didCompleteDownloadingImage:forPhoto:) onObject:mockDelegate] //we must call the protocol, as the real method would fail without network downloadLargeImage:[OCMArg any]]; //5. do the download [mockUtil downloadLargeImage:[OCMArg any]]; //6. make sure the delegate got the call [mockDelegate verify];//make sure out delegate method is actually called //assert! STAssertTrue(vc.currentSelectedPhotoObject!=nil, @"should have mock photo"); }
As one can see we can use OCMock to ease the burden of network dependency when building our apps. In a real world application, we would want to mock failure as well- you will find in the tests source code a test that demonstrates the api failure as well (testMockApiQueryFail). Happy mocking…
2 Comments