Infinite Loop

Using NSURLProtocol for Injecting Test Data

In earlier posts I described methods for unit testing asynchronous network access and how to use mock objects for further control of the scope of these unit tests. In this tutorial I’ll present an alternative way of providing reliable test data by customizing the NSURLProtocol class in order to deliver static test data.

A few months ago Gowalla made the networking code used in their iPhone client available as open source on GitHub. The AFNetworking library as it is called is a “A delightful iOS networking library with NSOperations and block-based callbacks“. One of the things that first caught my eye was the built-in support for accessing JSON based services with just a few lines of code.

The simplicity of the AFNetworking interface inspired me to give it a test spin and write ILBitly which provides an Objective C based wrapper for the Bitly url shortening service. It’s very easy to use AFNetworking and especially the JSON support that is accessed using a single class methods. Unfortunately this simplicity also makes it quite difficult to write self-contained unit and mock tests using OCMock. This is mainly because OCMock doesn’t support mocking of class methods. My attempts with other techniques such as method swizzling wasn’t successful either.

It wasn’t until a few days ago when I noticed a discussion on GitHub about how to properly mock the interface to AFNetworking. In the discussion Adam Ernst suggested to use a customized NSURLProtocol for doing the task. That finally gave me the missing clue on how to solve the testing problem.

[AdSense - blog banner]

Subclassing NSURLProtocol

As mentioned above I didn’t find any easy way to mock the interface to AFJSONRequestOperation in order to intercept the network access. So an alternative solution is to intercept the standard http protocol built into iOS. This is done by registering our own custom NSURLProtocol subclass capable of handling http requests: ILCannedURLProtocol. Since each registered protocol handler is asked in reverse order of registration our class will always be consulted before the standard classes.

The primary goal of ILCannedURLProtocol is to respond with a pre-loaded set of test data every time a http request is made. This way we’ll be able to remove any outside influences when running the tests. We’ll also be able to have the http request fail when we want it to fail. The interface for ILCannedURLProtocol is shown below:

@interface ILCannedURLProtocol : NSURLProtocol
+ (void)setCannedResponseData:(NSData*)data;
+ (void)setCannedHeaders:(NSDictionary*)headers;
+ (void)setCannedStatusCode:(NSInteger)statusCode;
+ (void)setCannedError:(NSError*)error;
@end

It is not able to fully replace any http requests in its current form. For instance it is only designed to intercept GET requests. Neither does it support any type of authentication challenge/response. But it provides enough functionality to deliver the test data needed for testing ILBitly and probably other similar classes.

Basically each of the setCannedXxx methods just retains the object passed to it so the object can be returned again when needed by a http request. This also means that it is only able to serve one set of test data at a time.

There are a few additional methods that need to be implemented when subclassing  NSURLProtocol. One of these is canInitWithRequest: This method is called every time a NSURLRequest is started in order to determine if that request is supported by the class. We’ll use that to intercept the http GET requests:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
  // For now only supporting http GET
  return [[[request URL] scheme] isEqualToString:@"http"]
         && [[request HTTPMethod] isEqualToString:@"GET"];
}

We also need to implement the startLoading method. This method is called once the appropriate protocol handler has been instantiated in order to service the request with data. Our method is able to either respond with a successful response or with an error depending on which of the canned data that has been set:

- (void)startLoading {
  NSURLRequest *request = [self request];
  id client = [self client];
 
  if(gILCannedResponseData) {
    // Send the canned data
    NSHTTPURLResponse *response = 
      [[NSHTTPURLResponse alloc] initWithURL:[request URL]
                                  statusCode:gILCannedStatusCode
                                headerFields:gILCannedHeaders 
                                 requestTime:0.0];
 
    [client URLProtocol:self didReceiveResponse:response
            cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [client URLProtocol:self didLoadData:gILCannedResponseData];
    [client URLProtocolDidFinishLoading:self];
 
    [response release];
  }
  else if(gILCannedError) {
    // Send the canned error
    [client URLProtocol:self didFailWithError:gILCannedError];
  }
}

If you decide to use the above code for testing in your own project you must make sure not to accidentally include it in the production code for any apps targeted for the App Store. If you haven’t already spotted the reason for this I’ll lead your attention to the initializer for NSHTTPURLResponse. This is a private api obtained by running class-dump on the iOS 4.3 SDK. If you include this call in your production code you therefore risk it being rejected by Apple. There is also a slight chance Apple might decide to modify it in future updates of iOS. But as long as it’s just used when running the unit tests everything should be fine.

Except for a few other methods which are basically empty that’s all there is to it. Now we’ll just need to register our custom class and load some canned data into it.

Preparing the Unit Tests

The unit test class for ILBitly just includes a few instance variables:

@interface ILBitlyTest : SenTestCase {
  ILBitly *bitly;
  id bitlyMock;
  BOOL done;
}
@end

The bitly variable contains an instance of the ILBitly code under test, bitlyMock holds the partial mock object for the ILBitly test, and done is used for signaling when the asynchronous calls have finished. These are explained more in details later.

Before every test case is executed the setUp method is automatically called allowing us to prepare things:

- (void)setUp
{
  [super setUp];
 
  // Init bitly proxy using test id and key - not valid for real use
  bitly = [[ILBitly alloc] initWithLogin:@"LOGIN" apiKey:@"KEY"];
  done = NO;
 
  [NSURLProtocol registerClass:[ILCannedURLProtocol class]];
  [ILCannedURLProtocol setCannedStatusCode:200];
}

We’ll use this method to prepare a default test instance as well as registering the ILCannedURLProtocol. The parameters used for initializing the ILBitly instance are just placeholders which are passed on to the service requests. Since we’ll be using static test data they have no real meaning except that we’ll verify later on that they are actually passed on as expected.

In order to balance things out properly, we’ll unregister our custom protocol as well as dispose of the test data after each test:

- (void)tearDown
{
  [NSURLProtocol unregisterClass:[ILCannedURLProtocol class]];
  [ILCannedURLProtocol setCannedHeaders:nil];
  [ILCannedURLProtocol setCannedResponseData:nil];
  [ILCannedURLProtocol setCannedError:nil];
 
  [bitly release];
  bitlyMock = nil;
 
  [super tearDown];
}

We’ll also need to prepare some test data. This can easily be done by using curl to save the raw response from bitly to a JSON file and loading that again for each test case as described in this previous post.

Putting it all Together

Finally we’ll need to write some tests that verifies the ILBitly code. As an example one of the tests for the shortening service is shown below:

- (void)testShorten {
  // Prepare the canned test result
  [ILCannedURLProtocol setCannedResponseData:[self cannedDataWithName:@"shorten"]];
  [ILCannedURLProtocol setCannedHeaders:
    [NSDictionary dictionaryWithObject:@"application/json; charset=utf-8" 
                                forKey:@"Content-Type"]];
 
  // Prepare the mock
  bitlyMock = [OCMockObject partialMockForObject:bitly];
  NSURL *trigger = [NSURL URLWithString:@"http://"];
  [[[bitlyMock expect] andReturn:[NSURLRequest requestWithURL:trigger]]
    requestForURLString:[OCMArg checkWithBlock:^(id url) {
      return [url isEqualToString:EXPECTED_REQUEST]; 
  }]];
 
  // Execute the code under test
  [bitly shorten:@"http://www.infinite-loop.dk/blog/" result:^(NSString *result) {
    STAssertEqualObjects(result, @"http://j.mp/qA7S4Q", @"Unexpected short url");
    done = YES;
  } error:^(NSError *err) {
    STFail(@"Shorten failed with error: %@", [err localizedDescription]);
    done = YES;
  }];
 
  // Verify the result
  STAssertTrue([self waitForCompletion:5.0], @"Timeout");
  [bitlyMock verify];
}

In the first part the static test data is loaded into the test protocol.

Next a partial mock object is created for the bitly object. Its primary role is to intercept the internal call to requestForURLString: and setup an expectation that it’s actually being called. Once that call is made it will verify that the expected url is requested and finally return an instance of NSURLRequest. That instance just contains enough of the basic url scheme in order to trigger the load of our custom protocol.

The code under test can now be executed as shown in the third part. Since the blocks may be called at any time after invoking the shorten:result:error: method done is set so we know when it has been called.

The final part of the code then waits for up to 5 seconds for done to be set as detailed in a previous post. Finally verify is called on the mock object to ensure that the expected messages were received.

If we instead want to test for proper handling of errors we’ll just have to replace the first part of the test method so it sets up error data and change the tests accordingly:

  [ILCannedURLProtocol setCannedError:
    [NSError errorWithDomain:NSURLErrorDomain
                        code:kCFURLErrorTimedOut
                    userInfo:nil]];

Conclusion

As I’ve shown above it’s possible to use NSURLProtocol for injecting predictable test data into unit and mock tests that would otherwise have been subject to external factors. It’s also possible to extend these tests even further. For instance you could use this method for implementing various simulations of bad network conditions such as high latencies and low bandwidth. The possibilities are endless and I just hope that this post at least have provided some inspiration.

The ILBitly wrapper as well as the accompanying test classes used in this post are available on GitHub along with a sample iPhone app that demonstrates some of the functionality.

Update: The ILCannedURLProtocol class is now included in the ILTesting repository on Github.

Comments and suggestions are welcome as always.

 

Comments (7) | Trackback

7 Responses to “Using NSURLProtocol for Injecting Test Data”

  1. [...] Using NSURLProtocol for Injecting Test Data [...]

  2. Ron Midthun says:

    As a note for anyone using this:
    For NSURLProtocol, overloading

    + (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest *)request

    is required according to Apple docs. I did not include this override and I was getting some very weird error message:
    warning: Attempting to create USE_BLOCK_IN_FRAME variable with block that isn’t in the frame.

    I was unable to find the exact cause of this error (even StackOverflow is baffled), but overriding that method seems to fix it for me. The override just returned the request.

    • Claus Broch says:

      Good point Ron,

      I didn’t mention the canonicalRequestForRequest: in the tutorial text but it is actually included in the source code for ILCannedURLProtocol available on GitHub and just returns the request as you mentions.

  3. Levi Brown says:

    Very very useful. Thanks for putting this together for the community.

  4. ash says:

    I believe you no longer have to use the private method, iOS 5 introduced:

    -(id)initWithURL:(NSURL*) url statusCode:(NSInteger) statusCode HTTPVersion:(NSString*) HTTPVersion headerFields:(NSDictionary*) headerFields;

    • Claus Broch says:

      That’s true. As of Mac OS X 10.7.2 and iOS 5.0 Apple has introduced a public initializer. If that initializer had been available at the time I wrote ILCannedURLProtocol I would certainly have used it instead. I will probably migrate to the public method eventually, but since ILCannedURLProtocol is not meant to be included in production code it doesn’t really do much harm to use the private method for the time being.

  5. Bob says:

    Is it possible that real HTTP messages still go through? I seem to have the behaviour whereby the canned protocol is used but real HTTP messages get through as well, from another thread. Not sure if its because I’m testing an NSOperation (via [operation start] or if its the registration of protocols? any ideas?

    Great post btw, I’m determined to get it working as its a much simpler approach to spinnung up a spoof HTTP server locally for testing.

Leave a Reply

*