Reputation: 33471
I am using OCMock to mock NSManagedObjects in my unit tests. I use Mogenerator to generate machine and human readable files for my core data objects. I am trying to mock an NSManagedObject to return a boolean value and a string. Both are attributes on my core data entity. When I mock the BOOL
, the value is returned from the object correctly, my class uses it, and the test passes successfully. When I attempt to stub an NSString
property on the same object, it throws an NSInvalidArgumentException [NSProxy doesNotRecognizeSelector]
.
Here is the calling code:
id biller = [OCMockObject mockForClass:[Biller class]];
// passes
[[[biller stub] andReturnValue:OCMOCK_VALUE((BOOL){NO})] billerValidatedValue];
// throws exception
[[[biller stub] andReturn:@"Test"] name];
Here is the exception:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy doesNotRecognizeSelector:name] called!'
I know there are some recommendations to have an interface that sits in front of the NSManagedObject for testing purposes, but this seems to be adding complexity on top of the Mogenerator machine/human files.
Are there any other suggestions around this without completely reconfiguring this code? This code is already in production, and we are trying to add unit tests in as we develop new features.
Upvotes: 1
Views: 1842
Reputation: 17762
The crux of the problem is that your Core Data model is not available in the tests, so when you try to stub the property read, that method does not exist. Core Data dynamically intercepts property accessors at runtime.
To make your model available, you need to make sure your .xcdatamodeld is included in your unit test target, and you need to set up the model in your test. I'm not sure you'll be able to mock dynamic properties, but it becomes trivial to do CRUD operations on Core Data objects in your tests, so there's no need to mock them. Here's one way to initialize your model in your tests:
static NSManagedObjectModel *model;
static NSPersistentStoreCoordinator *coordinator;
static NSManagedObjectContext *context;
static NSPersistentStore *store;
-(void)setUp {
[super setUp];
if (model == nil) {
@try {
NSString *modelPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"my-model" ofType:@"mom"];
NSURL *momURL = [NSURL fileURLWithPath:modelPath];
model = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];
}
@catch (NSException *exception) {
NSLog(@"couldn't get model from bundle: %@", [exception reason]);
@throw exception;
}
}
coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSError *error;
store = [coordinator addPersistentStoreWithType: NSInMemoryStoreType
configuration: nil
URL: nil
options: nil
error: &error];
assertThat(store, isNot(nil));
context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:coordinator];
}
-(void)tearDown {
// these assertions ensure the test was not short-circuited by a failure to initialize the model
assertThat(model, isNot(nil));
assertThat(context, isNot(nil));
assertThat(store, isNot(nil));
assertThat(coordinator, isNot(nil));
NSError *error = nil;
STAssertTrue([coordinator removePersistentStore:store error:&error],
@"couldn't remove persistent store: %@", [error userInfo]);
[super tearDown];
}
Alternatively, you could simplify things significantly by using MagicalRecord. Even if you don't use it in your app, you can use it in your tests to encapsulate all the core data setup. Here's what our unit test setup looks like in apps with MagicalRecord:
-(void)setUp {
[super setUp];
[MagicalRecordHelpers setupCoreDataStackWithInMemoryStore];
}
-(void)tearDown {
[MagicalRecordHelpers cleanUp];
[super tearDown];
}
Upvotes: 3