I’m toying with using Core Data to manage a graph of objects, mainly for dependency injection (a subset of the NSManagedObjects do need to be persisted, but that isn’t the focus of my question). When running unit tests, I want to take over creation of the NSManagedObjects, replacing them with mocks.
I do have a candidate means of doing this for now, which is to use the runtime’s method_exchangeImplementations to exchange [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:] with my own implementation (ie. returning mocks). This works for a small test I’ve done.
I have two questions regarding this:
- is there a better way to replace Core Data’s object creation than swizzling insertNewObjectForEntityForName:inManagedObjectContext? I haven’t forayed far into the runtime or Core Data, and may be missing something obvious.
- my replacement object creation method concept is to return mocked NSManagedObjects. I’m using OCMock, which won’t directly mock NSManagedObject subclasses because of their dynamic
@propertys. For now my NSManagedObject’s clients are talking to protocols rather than concrete objects, so I return mocked protocols rather than concrete objects. Is there a better way?
Here’s some pseudoish code to illustrate what I’m getting at. Here’s a class I might be testing:
@interface ClassUnderTest : NSObject
- (id) initWithAnObject:(Thingy *)anObject anotherObject:(Thingo *)anotherObject;
@end
@interface ClassUnderTest()
@property (strong, nonatomic, readonly) Thingy *myThingy;
@property (strong, nonatomic, readonly) Thingo *myThingo;
@end
@implementation ClassUnderTest
@synthesize myThingy = _myThingy, myThingo = _myThingo;
- (id) initWithAnObject:(Thingy *)anObject anotherObject:(Thingo *)anotherObject {
if((self = [super init])) {
_myThingy = anObject;
_myThingo = anotherObject;
}
return self;
}
@end
I decide to make Thingy and Thingo NSManagedObject subclasses, perhaps for persistence etc, but also so I can replace the init with something like:
@interface ClassUnderTest : NSObject
- (id) initWithManageObjectContext:(NSManagedObjectContext *)context;
@end
@implementation ClassUnderTest
@synthesize myThingy = managedObjectContext= _managedObjectContext, _myThingy, myThingo = _myThingo;
- (id) initWithManageObjectContext:(NSManagedObjectContext *)context {
if((self = [super init])) {
_managedObjectContext = context;
_myThingy = [NSEntityDescription insertNewObjectForEntityForName:@"Thingy" inManagedObjectContext:context];
_myThingo = [NSEntityDescription insertNewObjectForEntityForName:@"Thingo" inManagedObjectContext:context];
}
return self;
}
@end
Then in my unit tests I can do something like:
- (void)setUp {
Class entityDescrClass = [NSEntityDescription class];
Method originalMethod = class_getClassMethod(entityDescrClass, @selector(insertNewObjectForEntityForName:inManagedObjectContext:));
Method newMethod = class_getClassMethod([FakeEntityDescription class], @selector(insertNewObjectForEntityForName:inManagedObjectContext:));
method_exchangeImplementations(originalMethod, newMethod);
}
… where my []FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext] returns mocks in place of real NSManagedObjects (or protocols they implement). The only purpose of these mocks is to verify calls made to them while unit-testing ClassUnderTest. All return values will be stubbed (including any getters referring to other NSManagedObjects).
My test ClassUnderTest instances will be created within the unit tests thus:
ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];
(the context won’t actually be used in test, because of my swizzled insertNewObjectForEntityForName:inManagedObjectContext)
The point of all this? I’m going to be using Core Data for many of the classes anyway, so I might as well use it to help reduce the burden managing changes in constructors (every constructor change involves editing all clients including a bunch of unit tests). If I wasn’t using Core Data, I might consider something like Objection.
Looking at your sample code, it seems to me your test is getting bogged down in the details of the Core Data API, and as a result the test isn’t easy to decipher. All you care about is that a CD object was created. What I’d recommend is abstracting away the CD details. A few ideas:
1) Create instance methods in ClassUnderTest that wrap the creation of your CD objects, and mock them:
2) Create a convenience method in ClassUnderTest’s superclass, like
-(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;. Then you can mock calls to that method using a partial mock:3) Create a helper class that handles common CD tasks, and mock the calls to that class. I use a class like this in some of my projects:
These are trickier to mock, but you can check out my blog post on mocking class methods for a relatively straightforward approach.