Wednesday, May 25, 2011

Unit Testing an application with CoreData and RestKit.

EDIT: Even though this article was written for RestKit 0.9 with the Object Mapper 1.0, it works with RestKit 0.9 and the Object Mapper 2.0.

I recently had troubles with unit testing an iOS application which rely in RestKit and CoreData.
Since I didn't find a clear solution on the web, I share in this article how I solved this issue. It is, in fact, simpler that It may seem.
Note that for my unit tests, I use the SenTestKit framework provided by Apple with XCode4.

Setting up unit tests for CoreData

My application uses CoreData as a local cache. ResKit makes it easier to use CoreData to cache Rest responses therefore I use its RKManagedObject which is a subclass of NSManagedObject.

However, when It came to initialize CoreData support through RestKit in my tests, I ran into two issues.

The first looks like this:

'2011-05-22 13:47:00.431 otest[30023:903] CFPreferences: user home directory at file://localhost/Users/remy/Library/Application%20Support/iPhone%20Simulator/User/ is unavailable. User domains will be volatile.
Test Suite '/Users/remy/Library/Developer/Xcode/DerivedData/myapp-ios-gisnhxgfwfnvxlgscffpdhamroxy/Build/Products/Debug-iphonesimulator/myapp-tests.octest(Tests)' started at 2011-05-22 11:47:03 +0000
Test Suite 'BaseTestCase' started at 2011-05-22 11:47:03 +0000
Test Suite 'BaseTestCase' finished at 2011-05-22 11:47:03 +0000.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
Test Suite 'SessionTest' started at 2011-05-22 11:47:03 +0000
Test Case '-[SessionTest testLoginFailed]' started.
2011-05-22 13:47:03.881 otest[30023:903] *** Assertion failure in -[RKManagedObjectStore createPersistentStoreCoordinator], /Users/remy/Project/myapp/vendor/RestKit/Code/CoreData/RKManagedObjectStore.m:176


After some googling, I discovered that the issue was simply that when unit tests are initialized, the simulator path (/Users/remy/Library/Application%20Support/iPhone%20Simulator/User in this case) is not always already created.
And the same goes for the iPhone Simulator/User/Documents directory which is where the CoreData sqlite database is created.
Therefore a simple patch was to add the following piece of code in the -setUp method of my BaseTestCase class.

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
if (basePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:basePath] == NO)
[[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:nil];



Since RestKit with ObjectMapper 2.0, the Library/Caches directory is also needed

// Create the Library/Caches directory if the simulator doesn't have one (or the persistent store will fail to be created.)
paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
if (basePath != nil && [[NSFileManager defaultManager] fileExistsAtPath:basePath] == NO)
    [[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:nil];

I look for the Document directory and if it doesn't exists, I create it along with any intermediate directories in the path (in the case where the User directory does not exists).

The second issue I ran into was with the model file.
Normally, NSManagedObjectModel::mergedModelFromBundles is called by RestKit during the initialization of a new RKObjectManager. However, it looks like doing it in the unit tests does not work properly.
To solve this issue, I loaded my own NSManagedObjectModel and passed it to the initialization method of RestKit.
The piece of code looks like this:

// Force the loading of a specific object model since the OCUnit bundle is not able to do it using -mergedModelFromBundles:.
NSString *modelPath = [[NSBundle bundleForClass:[MyApplication class]] pathForResource:@"DataModel" ofType:@"mom"];
NSURL *modelURL = [NSURL fileURLWithPath:modelPath isDirectory:NO];
NSManagedObjectModel *managedObjectModel = [[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] autorelease];

// Initialize the object manager.
[RKObjectManager sharedManager].objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:@"mystore.sqlite" usingSeedDatabaseName:nil managedObjectModel: managedObjectModel];


The important detail here is to specify a class of the Application (and not of the test bundle) in the -bundleForClass: method. It is also necessary to add the .xcdatamodel file to the source files of the unit test bundle so it will be compiled and made available in the test bundle.

Mocking RestKit

Once Core Data was up and running, I wanted to be able to mock the remote API on which my application relies. I looked around and found OCMock but after some tests, I concluded that it was not the right solution to simply mock the REST API I was using in my applicaton. I didn't want to modify my application code and I felt that with ObjectiveC, I should not have to use dependency injection techniques which are often used in Java to use mock objects instead of the actual network aware implementation.
After some reading about the runtime and specific languages features of ObjC, I realized that it was actually incredibly simple to mock anything you want using implementation exchange or more simply, categories.

In spite of it, RestKit is a well designed object oriented framework and all the network communications are done using two classes: RKRequest and RKResponse.
To solve my issue, I wrote a category to RKRequest which provides a reimplementation of the -fireAsynchronousRequest method (since I use only asynchronous request, but there is also a -fireSynchronousRequest for those who need it).
The piece of code looks like this:

- (void)fireASynchronousRequest
{
// From the original implementation.

[self prepareURLRequest];
NSString* body = [[NSString alloc] initWithData:[_URLRequest HTTPBody] encoding:NSUTF8StringEncoding];
NSLog(@"Sending %@ request to URL %@. HTTP Body: %@", [self HTTPMethod], [[self URL] absoluteString], body);
[body release];

_isLoading = YES;
if ([self.delegate respondsToSelector:@selector(requestDidStartLoad:)])
[self.delegate requestDidStartLoad:self];

RKResponse* response = [[[RKResponse alloc] initWithRequest:self] autorelease];
[[NSNotificationCenter defaultCenter] postNotificationName:RKRequestSentNotification object:self userInfo:nil];

// Now execute the mocking code.
NSString *bundlePath = nil;
NSInteger statusCode = 200;

id requestJsonObject = nil;
if (self.params != nil)
{


  NSString *jsonString = [[[NSString alloc] initWithData:[self.params HTTPBody] encoding:NSUTF8StringEncoding] autorelease];


  // If you use an old version of restkit with Object mapper 1.0:
  requestJsonObject = [[[[RKJSONParser alloc] init] autorelease] objectFromString:jsonString];


  // If you use the latest version of RestKit with Object Mapper 2.0:
  id<RKParser> parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:RKMIMETypeJSON];
  NSAssert1(parser, @"Cannot perform object load without a parser for MIME Type '%@'", RKMIMETypeJSON);
  NSError **error = nil;
  requestJsonObject = [parser objectFromString:jsonString error:error];
}

// Switch to determine which to to use


// Prepare the results.
NSData *responseData = nil;
if (bundlePath == nil)
responseData = [NSData data];
else
{
// Load the mock file.
NSString *responseString = [NSString stringWithContentsOfFile:bundlePath encoding:NSUTF8StringEncoding error:nil];
responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
}



// Send the response. This is a superclass to be able to return a custom statusCode.
MyHTTPURLResponse *urlResponse = [[[MyHTTPURLResponse alloc] initWithURL:_URL MIMEType:@"application/json" expectedContentLength:[responseData length] textEncodingName:nil] autorelease];
[urlResponse setStatusCode:statusCode];

// Delegates in the order RestKit is expecting it.
[response connection:nil didReceiveResponse:urlResponse];
[response connection:nil didReceiveData:responseData];
if ([response isError] == YES)
[response connection:nil didFailWithError:[NSError errorWithDomain:@"test domain" code:statusCode userInfo:nil]];
else
[response connectionDidFinishLoading:nil];
}


That's all. Now you have no excuse to not unit test your application extensively!

4 comments:

  1. Hello, I think I'm experiencing a similar problem to the one you describe in the post.
    I modified RestKit to use an in memory store.
    Here is my setUp: https://gist.github.com/1438888

    The mapping fail on this assert:
    NSAssert(objectStore, @"Object store cannot be nil");

    In fact if I step through the setUp, and check
    [RKObjectManager sharedManager].objectStore it's nil.

    Do you have any idea of what's happening here?
    I know that my mods to RestKit are OK, since I've tested them
    separately.

    ReplyDelete
  2. Hi Carlo,
    I don't have enough code to know what happens but my guess if that the objectStoreInMemory method returns nil.
    One reason might be because it fails to load the data model bundle (.mom extension).

    ReplyDelete
  3. As lengthy as would possibly be} over the age of 21 and inside New 다파벳 York state traces, ready to|you'll be able to} wager and wager with any of the licensed sportsbooks in NY. A fresh and slick interface greets bettors upon arrivial, with SI sportsbook catering to all ranges of betting calibre. The newly launched operator boasts many of the user-friendly and in-demand options found on the 888 Sport platform. Launching in Colorado and Virginia, Sports Illustrated Sportsbook becomes one of many latest additions to the sports activities betting scene in the USA.

    ReplyDelete
  4. Yes, Fairspin Casino provides a simple login system in your account. You want to|might want to} click the "Login" button and enter your e-mail and password. Don't overlook to tick the special check for robots, which protects the online establishment from spam makes an attempt or hacker assaults. Alternatively, you can use use|you must use} social networks to shortly log in and begin your way on the earth of gambling. 메리트카지노 Big Spin Casino was first despatched off in 2017 and has became a well-known web-based on line casino for US, Canadian, Australian, and European gamers. With advancements on the top and games all via the basic page, that is another on line casino US gamers will appreciate.

    ReplyDelete