Take me home

Unit testing iOS: Core Data with multiple and/or versioned models

Published January 31, 2011

In my article iOS unit testing tips & tricks, I explained how I use an in-memory managed object context in my unit tests. This article updates some code from that section. The old code did not work in a project that used versioned models.

Specifying the model name

The old version of the script had the following in it:

[NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]]

This is problematic. If your project has a model with multiple versions, you will get an error saying Can't merge models with two different entities named 'MyEntityName'. This is because it finds all the versions of your model in [NSBundle allBundles], treats them as separate models, and tries to merge them. Merging two almost identical models will throw errors.

You can solve the problem by altering the code to only get the model you want to use:

// Old
model = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];

// New
static NSString *testTargetName = @"Tests";
NSBundle *testsBundle;
for (NSBundle *bundle in [NSBundle allBundles]) {
    // @"Tests" is the name of the test target.
    if ([[b objectForInfoDictionaryKey:@"CFBundleExecutable"] isEqualToString:testTargetName]) {
        testsBundle = bundle;
        break;
    }
}
       
NSURL *modelURL = [NSURL fileURLWithPath:[testsBundle pathForResource:@"YourModelName" ofType:@"momd"]];
self.model = [[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] autorelease];

The most significant change is that we now specify the model name. We could try guessing the model name, or get the first model we find and use it. But if we guess wrong, or you're in a project with multiple modules, we're screwed. So we might as well specify it. Replace @"YourModelName" with the name of the .xcdatamodel or .xcdatamodeld file. Off topic, but FYI: In Xcode's templates, the models is named after the project itself. It's in the Resources group.

The full code

// Header
@interface ManagedObjectContextTestHelper : NSObject {
}

-(id)initWithModelName:(NSString *)name;

@property (nonatomic, retain) NSManagedObjectModel *model;
@property (nonatomic, retain) NSPersistentStoreCoordinator *coordinator;
@property (nonatomic, retain) NSPersistentStore *store;
@property (nonatomic, retain) NSManagedObjectContext *context;

@end

// Implementation
@implementation ManagedObjectContextTestHelper
@synthesize model, coordinator, store, context;
-(id)initWithModelName:(NSString *)name {
    static NSString *testTargetName = @"Tests";
    
    if (self = [super init]) {
        NSBundle *testsBundle;
        for (NSBundle *bundle in [NSBundle allBundles]) {
            if ([[bundle objectForInfoDictionaryKey:@"CFBundleExecutable"] isEqualToString:testTargetName]) {
                testsBundle = bundle;
                break;
            }
        }
        
        NSURL *modelURL = [NSURL fileURLWithPath:[testsBundle pathForResource:name ofType:@"momd"]];
        self.model = [[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL] autorelease];
        self.coordinator = [[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model] autorelease];
        self.store = [coordinator addPersistentStoreWithType: NSInMemoryStoreType
                                          configuration: nil
                                                    URL: nil
                                                options: nil 
                                                  error: NULL];
        
        self.context = [[[NSManagedObjectContext alloc] init] autorelease];
        self.context.persistentStoreCoordinator = coordinator;

    }
    
    return self;
}

-(void)dealloc {
    [context release];
    [store release];
    [coordinator release];
    [model release];
    [super dealloc];
}
@end

Questions or comments?

Feel free to contact me on Twitter, @augustl, or e-mail me at august@augustl.com.