Unit Testing Core Data NSManagedObject with Kiwi

      5 Comments on Unit Testing Core Data NSManagedObject with Kiwi

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

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.

//
// NSManagedObject+UnitTests.h
//
@interface NSManagedObject (UnitTests)

-(NSNumber *)validateValue:(id)value forSelector:(SEL)selector;
-(NSPropertyDescription *)propertyBySelector:(SEL)selector;

@end

//
// 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.

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.

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.




5 thoughts on “Unit Testing Core Data NSManagedObject with Kiwi

  1. Victor Ilyukevich

    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 check error == nil in your category method 😉

    Thanks again for the article.

    Reply
    1. Dave Post author

      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.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *