Needless to say, I’m standing on the shoulders of giants here. Chris Hanson has written a great post on setting up the Core Data “stack” inside unit tests, Bill Bumgarner has written about their experiences unit-testing Core Data itself and PlayTank have an article about introspecting the object tree in a managed object model. I’m not going to rehash any of that, though I will touch on bits and pieces.
In this post, I’m going to look at one of the patterns I’ve employed to create testable code in a Core Data application. I’m pretty sure that none of these patterns I’ll be discussing is novel, however this series has the usual dual-purpose intention of maybe helping out other developers hoping to improve the coverage of the unit tests in their Core Data apps, and certainly helping me out later when I’ve forgotten what I did and why ;-).
Pattern 1: remove the Core Data dependence. Taking the usual example of a Human Resources application, the code which determines the highest salary in any department cares about employees and their salaries. It does not care about NSManagedObject instances and their values for keys. So stop referring to them! Assuming the following initial, hypothetical code:
- (NSInteger)highestSalaryOfEmployees: (NSSet *)employees {
NSInteger highestSalary = -1;
for (NSManagedObject *employee in employees) {
NSInteger thisSalary = [[employee valueForKey: @"salary"] integerValue];
if (thisSalary > highestSalary) highestSalary = thisSalary;
}
//note that if the set's empty, I'll return -1
return highestSalary;
}
This is how this pattern works:
- Create NSManagedObject subclasses for the entities.
@interface GLEmployee : NSManagedObject
{}
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *salary;
@property (nonatomic, retain) GLDepartment *department;
@endThis step allows us to see that employees are objects (well, they are in many companies anyway) with a set of attributes. Additionally it allows us to use the compile-time checking for properties with the dot syntax, which isn’t available in KVC where we can use any old nonsense as they key name. So go ahead and do that!
- (NSInteger)highestSalaryOfEmployees: (NSSet *)employees {
NSInteger highestSalary = -1;
for (GLEmployee *employee in employees) {
NSInteger thisSalary = [employee.salary integerValue];
if (thisSalary > highestSalary) highestSalary = thisSalary;
}
//note that if the set's empty, I'll return -1
return highestSalary;
} - Abstract out the interface to a protocol.
@protocol GLEmployeeInterface <NSObject>
@property (nonatomic, retain) NSNumber *salary;
@endNote that I’ve only added the salary to the protocol definition, as that’s the only property used by the code under test and the principle of YAGNI tells us not to add the other properties (yet). The protocol extends the NSObject protocol as a safety measure; lots of code expects objects which are subclasses of NSObject or adopt the protocol. And the corresponding change to the class definition:
@interface GLEmployee : NSManagedObject <GLEmployeeInterface>
{}
...
@endNow our code can depend on that interface instead of a particular class:
- (NSInteger)highestSalaryOfEmployees: (NSSet *)employees {
NSInteger highestSalary = -1;
for (id <GLEmployeeInterface> employee in employees) {
NSInteger thisSalary = [employee.salary integerValue];
if (thisSalary > highestSalary) highestSalary = thisSalary;
}
//note that if the set's empty, I'll return -1
return highestSalary;
} - Create a non-Core Data “mock” employee
Again, YAGNI tells us not to add anything which isn’t going to be used.@interface GLMockEmployee : NSObject <GLEmployeeInterface>
{
NSNumber *salary;
}
@property (nonatomic, retain) NSNumber *salary;
@end
@implementation MockEmployee
@synthesize salary;
@endNote that because I refactored the code under test to handle classes which conform to the GLEmployeeInterface protocol rather than any particular class, this mock employee object is just as good as the Core Data entity as far as that method is concerned, so you can write tests using that mock class without needing to rely on a Core Data stack in the test driver. You’ve also separated the logic (“I want to know what the highest salary is”) from the implementation of the model (Core Data).
OK, so now that you’ve written a bunch of tests to exercise that logic, it’s time to safely refactor that for(in) loop to an exciting block implementation :-).
Very cool article. I have been wanting to do this for a while, but being new to OC, I couldn't figure it out. You have shown me the light :-)
Now, what's up with this block implementation at the end? I don't get that.
Erwin
Erwin, glad you like the article. I just mean to say that now you have a unit-testing infrastructure in place, you can be much bolder about adopting new functionality. I haven't really thought about how I'd do a block-based transformation there, but the point is that with unit tests in place I have a safety net to catch any errors I introduce in doing the work.