4
votes

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:

  1. 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.
  2. 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.

4
Now this is allot of work. Stop for a minute and ask yourself: "I have spent X hours implementing mocked objects and unit tests, and this work has saved me Y hours in debugging and maintenance. Is Y greater than X, if not why should I continue wasting my time?" - PeyloW
@PeyloW: You can't just consider the time it has saved you to this point. If you did, it would never be considered productive use of your time because 30 seconds into creating a unit test, you wouldn't be done writing it and it would have saved you 0 seconds. X is > Y, and that is a "waste of time." Unit tests are designed to save you time in the future. It's a risk you have to consider before taking. They may not end up saving you time, especially if you scrap them on down the road. - FreeAsInBeer
@PeyloW Because unit-tests are great for maintaining an existing application. The hours that can be saved with a working unit tests is huge but your Y won't reflect that. - JustSid
@FreeAsInBeer & JustSid - So do a quick guess, what do you honestly believe your future Y will be? Are you writing tests to conform to a best practices, or to actually aid your work? Will it be less work to solve the problem when/if it arises, than invest time to write mocks and tests now? - PeyloW
@PeyloW: Both. Writing unit tests can help you conform to best practices as well as ensuring that your code accurately solves the given problems. However, one of the best advantages of unit tests is that they allow you change your code with confidence. You can make a change, and nothing breaks, you can be fairly confident that your changes are OK. It's very powerful knowing within minutes of making a change that your code still executes properly, as it did before the change. This advantage is even more pronounced when picking up a code base with which you aren't familiar. - FreeAsInBeer

4 Answers

3
votes

I find that there are generally 2 types of tests that involve Core Data entities: 1) testing methods that take an entity as an argument, and 2) testing methods that actually manage CRUD operations on the core data entities.

For #1, I do what it sounds like you're doing, as @graham-lee recommends: create a protocol for your entities, and mock that protocol in your tests. I don't see how it adds any extra code--you can define the properties in the protocol and have the entity class conform to the protocol:

@protocol CategoryInterface <NSObject>

@property(nonatomic,retain) NSString *label;
@property(nonatomic,retain) NSSet *items;
@property(nonatomic,retain) NSNumber *position;

@end

@interface Category : NSManagedObject<CategoryInterface> {}

@end

As to #2, I generally set up an in-memory store in my unit tests and just test do functional tests using an in-memory store.

static NSManagedObjectModel *model;
static NSPersistentStoreCoordinator *coordinator;
static NSManagedObjectContext *context;
static NSPersistentStore *store;
CategoryManager *categoryManager;

-(void)setUp {
    [super setUp];
    // set up the store
    NSString *userPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"category" ofType:@"momd"];
    NSURL *userMomdURL = [NSURL fileURLWithPath:userPath];
    model = [[NSManagedObjectModel alloc] initWithContentsOfURL:userMomdURL];
    coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
    store = [coordinator addPersistentStoreWithType: NSInMemoryStoreType
                                      configuration: nil
                                                URL: nil
                                            options: nil 
                                              error: NULL];
    context = [[NSManagedObjectContext alloc] init];

    // set the context on the manager
    [context setPersistentStoreCoordinator:coordinator];
    [categoryManager setContext:context];
}

-(void)tearDown {    
    assertThat(coordinator, isNot(nil));
    assertThat(model, isNot(nil));
    NSError *error;
    STAssertTrue([coordinator removePersistentStore:store error:&error], 
                 @"couldn't remove persistent store: %@", [error userInfo]);
    [super tearDown];
}

I verify in tearDown that the coordinator and model were successfully created, because I found that there were times when the creation threw an exception in setUp, so the tests weren't actually running. This will catch that kind of problem.

2
votes

Here is a blog posting on this: http://iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html

There is a training video on the ideveloper.tv site that mentions how to do unit testing in many of the cocoa frameworks including coredata: http://ideveloper.tv/store/details?product_code=10007

1
votes

I dislike mocks for Core Data because the object graph and the managed objects themselves can be to complex to accurately mock. Instead, I prefer to generate a full fledge reference store file and test against that. It's more work but the results are better.

Update:

is there a better way to replace Core Data's object creation than swizzling insertNewObjectForEntityForName:inManagedObjectContext?

If you just want to test the class i.e. a single instance in isolation, then you don't have to insert the object into a context at all. Instead, you can just initialize it like any other object. The accessors and other methods will work as normal but there is simply no context observing the changes and "managing" the object's relationship to other "managed" objects.

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?

It depends on what you are actually testing. If you are testing the NSManagedObject-subclass itself, then the mock protocol is useless. If you are testing other classes that communicate with or manipulate the managed object then the mock protocol will work fine.

The import thing to grasp when testing Core Data is that the tricky complexity in Core Data comes in the construction of the object graph at runtime. The getting and setting of attributes is trivial, it is the relationships and the key-value observing that gets complicated. You really can't mock the latter with any accuracy which is why I recommend creating a reference object graph to test against.

1
votes

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:

ClassUnderTest *thingyMaker = [ClassUnderTest alloc];
id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker];
[[[mockThingyMaker expect] andReturn:mockThingy] createThingy];

thingyMaker = [thingyMaker initWithContext:nil];

assertThat([thingyMaker thingy], sameInstance(mockThingy));

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:

ClassUnderTest *thingyMaker = [ClassUnderTest alloc];
id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker];
[[[mockThingyMaker expect] andReturn:mockThingy] createManagedObjectOfType:@"Thingy" inContext:[OCMArg any]];

thingyMaker = [thingyMaker initWithContext:nil];

assertThat([thingyMaker thingy], sameInstance(mockThingy));

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:

@interface CoreDataHelper : NSObject {}

+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;
+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate;
+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate sortedBy:(NSArray *)sortDescriptors;
+(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate sortedBy:(NSArray *)sortDescriptors limit:(int)limit;
+(NSManagedObject *)findManagedObjectByID:(NSString *)objectID inContext:(NSManagedObjectContext *)context;
+(NSString *)coreDataIDForManagedObject:(NSManagedObject *)object;
+(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;    

@end

These are trickier to mock, but you can check out my blog post on mocking class methods for a relatively straightforward approach.