The project that I am working on at the moment utilises Core Data for storing data, and that meant that I would need to figure out how to unit test the model. Most of what you are going to need to do is validate that your managed objects come back with appropriate defaults when first allocated, and that bounds checking is performed when saving.
Xcode’s Data Model Editor allows you to configure each entity as you please, but its devilishly easy to change one of those options without realising and radically alter the behaviour of your application. Unit Testing to the rescue!
I prefer to use the [Kiwi TDD/BDD Unit Testing framework](http://github.com/allending/kiwi) for my tests. Most of the approach for unit testing Core Data will be familiar to non-Core Data objects, but there are a couple of tricks worth noting along the way. I’ll assume that you are familiar with unit testing and Kiwi for the purpose of this article. You can read some of my other articles on [Unit Testing Objective-C](/category/technology/unit-testing) if you need more background.
### Creating the Testing Environment
Our first task is to create a suitable test environment. We want to use the applications active data model, but we don’t want to use its persistent store, as we don’t want existing test data affecting the unit tests, and likewise we might create garbage in the course of testing. Also, the SQLite Persistent Store may be quick, but its much slower than necessary for unit testing purposes.
#!objectivec
describe(@”MyManagedObject”, ^{
__block MyManagedObject *sut;
__block MyAppDelegate *appDelegate;
__block MyStorageService *storageService;
beforeEach(^{
appDelegate = [[MyAppDelegate alloc] init];
[appDelegate stub:@selector(storageTypeForStorageService:) andReturn:NSInMemoryStore];
storageService = [MyStorageService alloc] initWithDelegate:nil dataSource:appDelegate];
sut = [storageService insertNewObjectForEntityName:@”MyManagedObject”];
});
});
In the above code we’ve set the scene. I use a couple of helpers when implementing Core Data, so the above probably needs a little explanation. `MyAppDelegate` is the regular `UIApplicationDelegate` and this acts as a data source delegate for a class called `MyStorageService`. This class acts as a thin wrapper to the Core Data Stack and provides a cleaner interface to pass around the application than the usual `NSManagedObjectContext`.
In the `beforeEach` that wraps all of the test cases, I’ve allocated an instance of `MyAppDelegate`.
**Important**: Don’t use the live instance that is loaded as the application test runner, which you might be tempted to obtain via a singleton. In my case, stubbing the Core Data methods would upset the rest of the application. Unit Testing like this will help improve you class design for testability.
On the `appDelegate` instance I’ve chosen to stub a method that provides the persistent store type to the storage service. By stubbing its return value, I know that the test of the model is being loaded identically to the way the application uses it, with the exception that I’m using a `NSInMemoryStoreType` so no data is written to the SQLite persistent store created by the application. My unit tests can play fast and loose with the data created in the knowledge that the store is empty for each test case, and destroyed afterwards.
I then allocate an instance of the class under test (`sut`) based on the data model, so its getting the basic behaviour defined by the model. Thats what we want to be testing next.
### Unit Test Category on NSManagedObject
In order to cut down on the verbosity of the unit test, I implemented a small category on `NSManagedObject` to provide a couple of helpers.
#!objectivec
//
// NSManagedObject+UnitTests.h
//
@interface NSManagedObject (UnitTests)
-(NSNumber *)validateValue:(id)value forSelector:(SEL)selector;
-(NSPropertyDescription *)propertyBySelector:(SEL)selector;
@end
#!objectivec
//
// NSManagedObject+UnitTests.m
//
@implementation NSManagedObject (UnitTests)
-(NSNumber *)validateValue:(id)value forSelector:(SEL)selector {
return @([self validateValue:&value forKey:NSStringFromSelector(selector) error:nil]);
}
-(NSPropertyDescription *)propertyBySelector:(SEL)selector {
return self.entity.propertiesByName[NSStringFromSelector(selector)];
}
### Testing the Entity
The first step is to make sure that the entity is configured correctly in the model.
#!objectivec
it(@”should not be an abstract entity”, ^{
[[theValue(sut.entity.isAbstract) should] beNo];
});
it(@”should not have a parent entity”, ^{
[[sut.entity.superentity should] beNil];
});
it(@”should not have compound indexes”, ^{
[[sut.entity.compoundIndexes should] beNil];
});
it(@”should not save an invalid instance”, ^{
NSError *error;
[storageService save:&error];
[[error should] beNonNil];
});
it(@”can save with valid data”, ^{
sut.firstname = @”dave”;
sut.lastname = @”meehan”;
[storageService save:&error];
[[error should] beNil];
});
In the above, we have tested properties of the entity to ensure that the model matches our expectations, and done a very basic couple of tests to check save behaviour with new instances.
### Testing Managed Object Attributes
Our next step is to ensure that correct initialisation and validation occurs on each property of the managed object.
#!objectivec
context(@”-firstname”, ^{
it(@”has no default”, ^{
[[sut.firstname should] beNil];
});
it(@”cannot be nil”, ^{
[[[sut validateValue:nil forSelector:@selector(firstname)] should] beNo];
});
it(@”cannot be empty”, ^{
[[[sut validateValue:@”” forSelector:@selector(firstname)] should] beNo];
});
it(@”is indexed”, ^{
[[@([sut propertyBySelector:@selector(firstname)].isIndexed]) should] beYes];
});
it(@”is not indexed by Spotlight”, ^{
[[@([sut propertyBySelector:@selector(firstname)].isIndexedBySpotlight]) should] beNo];
});
});
I hope you can see that there are behavioural tests that you can do to verify the model configuration (actual manipulation of property values) as well as tests on the model attributes. Attributes such as indexing would be very difficult to test through Core Data, as many are just hints to Core Data of how to perform, rather than specifics. By checking that the setting is as desired, you’ve done most of the work.
### Conclusion
The only issue that I have with this approach is that its quite long-winded. I’d prefer it if Kiwi could do some of the heavy lifting and repeat aspects of the tests, but as of the current release (2.2) that is not something that Kiwi is able to help with.
Dave, thanks for the article!
I’m just curious what
#!objectivec
line does in your category on NSManagedObject code snippet?And
NSManagedObject#validateValue:forKey:error:
returns BOOL which says if the value valid or not. Looks like there is no need to checkerror == nil
in your category method 😉Thanks again for the article.
Victor, thanks for the reply, glad you liked it.
The
#!objectivec
shouldn’t have been there – it was meant to be a blank line and that’s a directive to the source code plugin I use to set the language.Also, you are right about the
validateValue:forKey:error:
, checking the return value is sufficient, so I’ve amended the code to show it.Thanks, Dave!
Just a typo:
return
is missed now invalidateValue:forSelector:
😉Well spotted, I should pay more attention!
Great job!