Adoption Curve Dot Net

Unit Testing Core Data

| Comments

The project I’m working on at the moment is Core Data-based, and I’m (attempting) to built it using a test-driven approach. In practice, there’s at least as much development-driven testing as there is test-driven development, but I’m using the fact that I’m on a learning curve as my get-out-of-jail-free card.

I’m using GHUnit and OCMock as my test frameworks. Testing the view controllers and so on is fairly straight-forward so far, but I hit a major bump in the road when I started to try and test the Core Data related parts.

While the app itself was working fine, the tests steadfastly refused to return any data. No errors, just empty result sets.

What I’d omitted to do in my testing frenzy was set up the Core Data environment correctly in my tests. You can’t, as it turns out, rely on the app code creating it – you have to do so explicitly within your test suite.

For the benefit of my outsourced memory and any subsequent Google hits, this is how I got it to work.

Firstly, you need to import the CoreData headers:

#import <CoreData/CoreData.h>

and the headers for your NSManagedObject classes:

#import "Trial.h"

Then, add and synthesise a property to hold the Managed Object Context when it’s created:

@property (nonatomic, retain) NSManagedObjectContext *moc;
@synthesize moc;

Because I’m using GHUnit, the methods in my test templete looks like this:

-(void)testSomething {
  // This test should do something eventually
}

#pragma mark - Housekeeping

- (BOOL)shouldRunOnMainThread {
    // By default NO, but if you have a UI test or test dependent on running on the main thread return YES.
    // Also an async test that calls back on the main thread, you'll probably want to return YES.
    return NO;
}

- (void)setUpClass {
    // Run at start of all tests in the class
}

- (void)tearDownClass {
    // Run at end of all tests in the class
}

- (void)setUp {
    // Run before each test method
}

- (void)tearDown {
    // Run after each test method
}

The key methods are setupClass and tearDownClass, which get run at the start and end of the test suite. These is where you create and destroy the Managed Object Context:

- (void)setUpClass {
    // Run at start of all tests in the class
    NSManagedObjectModel *mom = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];

    GHAssertTrue((int)[psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL], @"Should be able to add in-memory store");

    self.moc = [[NSManagedObjectContext alloc] init];
    self.moc.persistentStoreCoordinator = psc;

    [self createSeedData];
}

There’s a subtlety when creating the managed object model – this needs to load all bundles, hence the [NSBundle allBundles] parameter.

- (void)tearDownClass {
    // Run at end of all tests in the class
    self.moc = nil;
}

This creates a sandbox MOC which you can mess about with to your heart’s content – doing things like seeding a data set:

-(void)createSeedData {

    NSArray *dataArray = [NSArray arrayWithObjects:@"One", @"Two", @"Three", @"Four", @"Five", @"Six", @"Seven", @"Eight", nil];

    for (int i=0; i < [dataArray count]; i++) {

        Trial = [Trial alloc] initWithData:[dataArray objectAtIndex:i];

        // Set up each trial - 4 get "inprogress" status
                // and 3 get "completed" status

    }

}

What I’m doing here is creating 8 Trial objects, and setting 4 of them as in progress and 3 as completed (the detail isn’t that relevant for this example). This gets used later to test that the predicates selecting the Trials is working correctly.

From here, it’s simple enough to start testing (because I’m paranoid, I added a test to check that the seed data was created correctly):

-(void)testSeedDataIsCreatedCorrectly {

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Trial" inManagedObjectContext:self.moc];
    [fetchRequest setEntity:entity];

    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"id" ascending:NO];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    NSError *error = nil;
    NSMutableArray *mutableFetchResults = [[self.moc executeFetchRequest:fetchRequest error:&error] mutableCopy];
    if (mutableFetchResults == nil) {
        NSLog(@"TrialSelectionVC: executeFetchRequest error: %@", [error localizedDescription]);
    }

    int resultsCount = [mutableFetchResults count];
    GHAssertEquals(resultsCount, 8, @"should be 8 objects in the test data, found %d", [mutableFetchResults count]);
}

Testing the relevant bit of the view controller means injecting the test MOC into the class. In the production code, the MOC that’s assigned to the view controller’s managedObjectContext is the one that gets created in the AppDelegate – which obviously I don’t have here:

-(void)testTrialsVCretrievesData {
    // Create and fetch trials
    TrialSelectionVC *trialVC = [[TrialSelectionVC alloc] init];

    trialVC.managedObjectContext = self.moc;

    NSMutableArray *results = [trialVC fetchTrials];

    // Assert results are returned
    GHAssertNotNil(results, @"results array is nil");

    // Assert that there should be three results
    GHAssertEquals((int)[results count], 3, @"there should be 3 results, %d were returned", [results count]);

}

On the face of it, this seems like an awful lot of code just for testing purposes – but it’s paid back as far as I’m concerned. By being able to extract and test the data selection elements of the application in isolation, I’m not having to do that by running the app itself.

It also means that I can afford to bugger about with my data schema, and be able to get an instant check on whether I’ve broken things downstream. In an ideal world that wouldn’t happen anyway, but at least now I’ve got some assurance that the app is somewhat less brittle than it might otherwise be.

Comments