iOS Unit Testing with Xcode 4 and Core Data

March 24th, 2011 § 16 Comments

Yes, there is support for writing unit tests in xcode but it is not straightforward to set it up and not easy to make it work with Core Data. Unfortunately, unit testing is not documented much. Here I would like to share my experience with adding unit tests to an existing project, and a few tips for unit testing Core Data code.

Xcode 4 User Guide has a brief, useful section about unit test setup. Go to Organizer and find “Run Unit Tests to Find Regressions” section, or click the link on it to access online version.

First, select your project on the navigation panel as shown below to add a new target for unit tests. Follow the screens and create the target.

After unit test target is created, go to Build Phases tab as seen below. You need to add items into the sections as you write unit tests that references different classes and resources in your project.

Unit Test Target

I am going to add a unit test class for Core Data. I need to add Core Data library, classes that I reference from my project, and also model file. I will do that after I create the test class to show everything at once.

First, let’s add a unit test class to test my Core Data access class. The class under test is called DataStore, so I name the test class as DataStoreTest. Do not forget to choose AppUnitTests as the group, and check the target for it and uncheck the target for the main app. Files should be created under unit test folder (AppUnitTests folder in this case).

I remove the auto generated codes, and write a test case to test the method that writes an object into the data store. Here is the code in DataStoreTest.m:

//
//  DataStoreTest.m
//  StudyUITableView
//
//  Created by Deniz Demir on 3/24/11.
//  Copyright 2011 Deniz Demir. All rights reserved.
//

#import "DataStoreTest.h"
#import "DataStore.h"
#import "Conversation.h"
#import "ConversationEntity.h"

@implementation DataStoreTest

static int conversationCount = 0;

- (void) setUp {
    [super setUp];
    // Set-up code here.
}

- (void) tearDown {
    // Tear-down code here.
    [super tearDown];
}

- (void) testWriteConversation {
    Conversation *conversation = [[Conversation alloc] init];
    NSString *cid = [NSString stringWithFormat:@"unittest_conv_%d", conversationCount++];
    [conversation setConversationId:cid];
    [conversation setPartnerDisplayName:@"Unit Test"];
    [conversation setPartnerDefaultAddress:@"unit@conversationunittest.net"];

    [[DataStore sharedInstance] writeConversation:conversation];
    ConversationEntity *entity = [conversation entity];

    STAssertNotNil(entity, @"entity is nil after conversation has been written...");
}
@end

Now, we need to return to AppUnitTests target and add the necessary items into various sections of Build Phases:

  • Compiled Sources: add implementation files of all imported header files.
  • Link Binary With Libraries: add all libraries needed by test codes (in this example, we need to add Core Data framework).
  • Copy Bundle Resources: add test class header files and any resources referenced by test classes (in this case, we need two header files and Core Data model file)

At the end, it looks like below for this example:

Note that you can change the target membership of a source file using file inspector on utilities panel on the right by selecting the file on the left navigation panel. However, Xcode 4 does not allow to change header files’ target membership using utilities panel. You can add header files into unit test target by adding it into Copy Bundle Resources section on target Build Phases as described above.

The last step we need to do is to configure the scheme for the app so that it can run unit tests. Go to Product->Edit Scheme from Xcode menu. Select Test section on the left panel, and add AppUnitTests into the Tests list as shown below (if you expand AppUnitTests, you should see test classes and test cases). When you add a test target here, it automatically adds it into Build section as well (this is why you see below it says 2 targets in Build section). You can double check your target there.

OK, now we are done with settings. You can now try to run the unit tests by choosing Test on run button on the top-left corner of Xcode. Opps! it fails with the following error:

It is because the some other files are indirectly referenced by unit test codes. We need to add such files into unit test target as well. To solve the error above, we need to add target membership of AppUnitTests for Message.m source file as shown below by using file inspector (no need to do anything with the header file):

Tips for Core Data

During the initialization of Core Data stack (objects of NSManagedObjectContext, NSManagedObjectModel, and NSPersistentStoreCoordinator), model object is usually created with the following call:

managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];

Passing nil for mergedModelFromBundles is meant to search for all bundles under app but it does not work with unit test targets. Passing [NSBundle allBundles] solves the issue:

managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]] retain];

It can also be solved by adding the bundle of unit test target into the bundle array passed to mergedModelFromBundles; in this example, adding what [NSBundle bundleWithIdentifier:@"com.yourcompany.AppUnitTests"] into bundle array. The following illustrates it:

managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:[NSArray arrayWithObject:[NSBundle bundleWithIdentifier:@"com.yourcompany.AppUnitTests"]]] retain];

Another issue that I had with Core Data unit testing is with getting document directory path for creating data store file. The document directory is returned by the following call:

NSString *dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

However, in unit test execution, it returns a directory where unit test cannot write, therefore, the call to add persistent store fails with the following error:

Unresolved error Error Domain=NSCocoaErrorDomain Code=258 "The operation couldn’t be completed. (Cocoa error 258.)" UserInfo=0x67871b0 {NSUnderlyingException=Error validating url for store}, {
NSUnderlyingException = "Error validating url for store";
}

I have found a workaround solution by replacing the line with this one:

NSString *dir = [[NSFileManager defaultManager] currentDirectoryPath];

This is far from a viable solution because the above line of code cannot be used in the app, so I need to change it when I run the app. But it at least helps me run the tests for now…

OK, it has been a long post but I hope it helps others save time.

Please leave comment if you see any issues or have any feedback.

§ 16 Responses to iOS Unit Testing with Xcode 4 and Core Data

  • Blanch says:

    Excellent tips! I have been seeking for some thing similar to this for quite a while these days. Thank you!

  • I should have found your article when you published it. It would have saved me one week’s detective work.

    I missed one or two things that you are pointing out but I completely fail on your last observation and suggestion for solution.

    You suggest:
    NSString *dir = [[NSFileManager defaultManager] currentDirectoryPath];

    But in my code, I look up the path and the method returns an NSURL:

    - (NSURL *)applicationDocumentsDirectory
    {
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    }

    which is later used:

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataTDD.sqlite"];

    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])

    Instead, I tried to use your suggestion to find some valid place to put the sqlite file, but transforming that NSString to a valid file NSURL only returns null.

    Are you using another Xcode template for the PersistentStoreCoordinator than what is supplied with the Xcode 4 Core Data templates?

    /johan

  • denizdemir says:

    Right, I haven’t used the template because I have added core data to an existing project. I do not know about the code generated by xcode in the template but here is what I wrote to create NSPersistentStoreCoordinator instance:

        NSString *dir = [[NSFileManager defaultManager] currentDirectoryPath];
        NSURL *storeURL = [NSURL fileURLWithPath:[dir stringByAppendingPathComponent:@"DataStore.sqlite"]];
        
        NSError *error = nil;
        persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
        NSPersistentStore *ps = [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];
    

    Just let me know if you have questions with the lines above. Thanks for your comment.

    -Deniz

  • I am certain that I tried that yesterday, but it failed for some reason. It could have been something else. Anyway, it worked outadabox this morning.

    Adding .m files in section “Compile sources” in this image:
    http://denizdemir.files.wordpress.com/2011/03/unittest_target_configured.png

    Seems to be the same as adding a file to a target, as you point out in this image:
    http://denizdemir.files.wordpress.com/2011/03/target_membership_source_file.png

    also
    It worked without adding the Model.xcdatamodel and InfoPlist.string to Copy bundle resources. However, I added it after reading your post.

    To summarize the code changes I have implemented from the xcode 4 core data template:


    /**
    Returns the managed object model for the application.
    If the model doesn't already exist, it is created from the application's model.
    */
    - (NSManagedObjectModel *)managedObjectModel
    {
    if (ivManagedObjectModel != nil)
    {
    return ivManagedObjectModel;
    }
    // first try TODO: Use when app is run
    // NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"bartdb" withExtension:@"momd"];
    // ivManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];

    // second try TODO: Only for unit test
    NSArray *bundles = [NSArray arrayWithObject:[NSBundle bundleForClass:[self class]]];
    ivManagedObjectModel = [NSManagedObjectModel mergedModelFromBundles:bundles];

    return ivManagedObjectModel;
    }

    and


    /**
    Returns the URL to the application's Documents directory.
    */
    - (NSURL *)applicationDocumentsDirectory
    {
    // first try TODO: Use when app is run
    //return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];

    // second try TODO: Only for unit test
    NSString *dir = [[NSFileManager defaultManager] currentDirectoryPath];
    return [NSURL fileURLWithPath:dir];
    }

    Thanks for a great article!
    /johan

    • denizdemir says:

      I don’t know why it does not work for you. It looks to me the same as mine. You may try to see the path string of NSURL object. Put breakpoint on the line that creates persistent store and print description of NSURL object dataStore. Mine shows a place under project directory, like this:

      .../StudyUITableView/DataStore.sqlite
      

      Regarding adding a source file to unit test target, yes, you’re right, they both do the same trick. For the data model file, I think the way I merge the bundles (i.e. all bundles), there is no need to put it into the bundle resources. However, I am not sure about the way you did in the code you’ve put in your last comment. It looks like it reads only bundle of test target but there might be something I am not aware of.

      -Deniz

  • pretty valuable stuff, overall I imagine this is worthy of a bookmark, thanks

  • [...] Demir helped me out there – a little Google brought up his post from late March, where he explained his troubles with Core Data and unit testing. Applying his fix inre: the [...]

  • mr.pppoe says:

    Thank you for the tips! I am seeking for the answer about the nil managedObjectModel problem these days.

    But the last tip on applicationDocumentsDirectory doesn’t solve my problem, it seems not the best solution.

  • [...] methods for Core Data to operate differently during command-line tests as described in iOS Unit Testing with Xcode 4 and Core Data; not covered here is how exactly to detect if you’re in a test, which I did by checking the [...]

  • Dustin says:

    Great post. For those hoping to leave some permanent code in place that will allow Core Data to work in all scenarios (on a device, from an interactive test target in Xcode, or from the command line), making the changes above conditional on an environment variable like WRAPPER_NAME containing “octest” seems to work just fine. I’ve posted an example of this as I was setting this up from a Jenkins CI perspective if anyone needs it.
    http://lifeandcode.net/2011/12/automated-ios-jenkins-builds-with-application-tests-and-core-data/

  • [...] Automation for iOS Applications * Tools Workflow Guide for iOS: About the Tools Workflow for iOS * iOS Unit Testing with Xcode 4 and Core Data * Running Xcode 4 unit tests from the command line Related Content:Automated iPhone Testing using [...]

  • Balaram Sundaram says:

    superb tutorial! should have found this post long before…. Will help me a lot!!!

    Cheers!

  • rdclark says:

    Thanks for this! It It worked for an unversioned data model, but failed when I went to a versioned one (see http://iphonedevelopment.blogspot.com/2009/09/core-data-migration-problems.html). Fortunately, the solution is straightforward: rather than merging from all bundles, find the app’s bundle at testing time and get the compiled data model from it.

    Replace:
    managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
    with:
    NSString *path = [[[NSBundle allBundles] lastObject] pathForResource:@”Foo” ofType:@”momd”];
    NSURL *momURL = [NSURL fileURLWithPath:path];
    managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];

    where “Foo” should be the name of your data model’s directory.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

What’s this?

You are currently reading iOS Unit Testing with Xcode 4 and Core Data at Deniz Demir's Blog.

meta