Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test Core Data Application

How should I test the findByAttribute instance method I added to NSManagedObject?

At first, I thought of programmatically creating an independent Core Data stack as demonstrated by Xcode's Core Data Utility Tutorial. And, in my search for that documentation, I came across Core Data Fetch Request Templates and thought that maybe instead of creating the method I made, I should make fetch request templates, but it doesn't look like the entityName can be variable with a fetch request template, can it? Can I create a fetch request template on NSManagedObject so that all subclasses can use it? Hmm, but then I would still need an entityName and I don't think there's a way to dynamically get the name of the subclass that called the method.

Anyway, it looks like a good solution is to create an in-memory Core Data stack for testing, independent from the production Core Data stack. @Jeff Schilling also recommends creating an in-memory persistent store. Chris Hanson also creates a persistent store coordinator to unit test Core Data. This seems similar to how Rails has a separate database for testing. But, @iamleeg recommends removing the Core Data dependence.

Which do you think is the better approach? I personally prefer the latter.

UPDATE: I'm unit testing Core Data with OCHamcrest and Pivotal Lab's Cedar. In addition to writing the code below, I added NSManagedObject+Additions.m and User.m to the Spec target.

#define HC_SHORTHAND
#import <Cedar-iPhone/SpecHelper.h>
#import <OCHamcrestIOS/OCHamcrestIOS.h>

#import "NSManagedObject+Additions.h"
#import "User.h"

SPEC_BEGIN(NSManagedObjectAdditionsSpec)

describe(@"NSManagedObject+Additions", ^{
    __block NSManagedObjectContext *managedObjectContext;   

    beforeEach(^{
        NSManagedObjectModel *managedObjectModel =
                [NSManagedObjectModel mergedModelFromBundles:nil];

        NSPersistentStoreCoordinator *persistentStoreCoordinator =
                [[NSPersistentStoreCoordinator alloc]
                 initWithManagedObjectModel:managedObjectModel];

        [persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType
                                                 configuration:nil URL:nil options:nil error:NULL];

        managedObjectContext = [[NSManagedObjectContext alloc] init];
        managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator;

        [persistentStoreCoordinator release];
    });

    it(@"finds first object by attribute value", ^{

        // Create a user with an arbitrary Facebook user ID.
        NSNumber *fbId = [[NSNumber alloc] initWithInteger:514417];
        [[NSEntityDescription insertNewObjectForEntityForName:@"User"
                                      inManagedObjectContext:managedObjectContext] setFbId:fbId];
        [managedObjectContext save:nil];

        NSNumber *fbIdFound = [(User *)[User findByAttribute:@"fbId" value:(id)fbId
                                                  entityName:@"User"
                                      inManagedObjectContext:managedObjectContext] fbId];

        assertThatInteger([fbId integerValue], equalToInteger([fbIdFound integerValue]));

        [fbId release];
    });

    afterEach(^{
        [managedObjectContext release];
    }); 
});

SPEC_END

If you can tell me why if I don't cast to (id) the fbId argument passed to findByAttribute I get

warning: incompatible Objective-C types 'struct NSNumber *',
expected 'struct NSString *' when passing argument 2 of
'findByAttribute:value:entityName:inManagedObjectContext:' from
distinct Objective-C type

then you get bonus points! :) It seems that I shouldn't have to cast an NSNumber to an id if the argument is supposed to be an id because NSNumber is an id, right?

like image 580
ma11hew28 Avatar asked Oct 11 '22 12:10

ma11hew28


1 Answers

My personal philosophy is that a test is not a test if it doesn't test the real thing so I look askance at any method that test fragments in isolation. Although it will work in many cases, especially in procedural code, it is likely to fail in complex code such as that found in Core Data object graphs.

Most of the points of failure in Core Data come from a bad data model e.g. missing a reciprocal relationship so that the graph comes out of balance and you have orphaned objects. The only way to test for a bad graph is to create a known graph and then stress test your code to see if it can find and manipulate the objects in the graph.

To implement this type of test I do the following:

  1. Start each test run by deleting the previously existing Core Data store file such that the store always begins in a known state.
  2. Provide a new store for each run, preferably by generating it in code each time but you can just swap in a copy of the store file before each run. I prefer the former method because its actually easier in the long run.
  3. Make sure the test data contains relatively extreme examples e.g. long names, strings with garbage characters, very big and very small numbers etc.

The state of the test object graph should be absolutely know at the time of each test. I routinely dump the entire graph in testing and I have methods to dump both entities and live objects in detail.

I usually develop and test an app's entire data model in a separate app project setup to do nothing but develop the data model. Only once I have the data model working exactly as needed in the app do I move it to the full project and begin to add controllers and interface.

Since the data model is the actual core of a properly implemented Model-View-Controller design app, getting the data model correct covers %50-%75 of development. The rest is a cake walk.

In this particular case, you really only need to test that the predicate of the fetch request returns the proper objects. The only way to test that is to provide it with a full test graph.

(I would note that this method is really pretty useless in practice. It won't return any particular object by attribute but merely any one of an arbitrary number of objects that have an attribute of that value. E.g. If you have an object graph with 23,462 Person objects with the firstName attribute value of John, this method will return exactly one arbitrary Person entity out fo 23,462. I fail to see the point of this. I think you are thinking in procedural SQL terms. That will lead to confusion when dealing with an object -graph manager like Core Data.)

Update:

I'm going to guess that your error is caused by the complier looking at the use of value in the predicate and assuming it must be a NSString object. When you drop an object in a string format, like that used by predicateWithFormat:, the actual value returned is an NSString object containing the results of the description method of the object. So, to the compiler you predicate actually looks like this:

[NSPredicate predicateWithFormat:@"%K == %@", (NSString *)attribute, (NSString *)value]

... so when it works backwards it will be looking for an NSString in the value parameter even though technically it shouldn't. This use of id is really not best practice because it will accept any class but you don't actually always know what the description string returned by the instance's -description method will be.

As I said above, you have some conceptual problems here. When you say in the comment below:

My intention was to make a method analogous to ActiveRecord's find_by_ dynamic finder.

... you are approaching Core Data from the wrong perspective. Active Record is largely an object wrapper around SQL to make it easier to integrate existing SQL servers with Ruby on Rails. As such it is dominated by procedural SQL concepts.

That is the exact opposite approach used by Core Data. Core Data is first and foremost an object graph management system for creating the model layers of a Model-View-Controller app design. As such, the objects are everything. E.g. It is even possible to have objects without attributes, only relationships. Such objects can have very complex behaviors as well. That is something that really doesn't exist in SQL or even Active record.

It is quite possible to have an arbitrary number of objects with the exact same attributes. This makes the method you are trying to create worthless and dangerous because you will never know which object you will get back. That makes it a "chaotic" method. If you have several objects with the same attribute, the method will arbitrarily return any single object that matches attribute value provides.

If you want to identify a particular object, you need to capture the object's ManagedObjectID and then use -[NSManagedObjectContext objectForID:] to retrieve it. Once an object has been saved, its ManagedObjectID is unique.

However, that feature is usually only used when you have to refer to objects in different stores or even different apps. There is usually no point otherwise. In using Core Data you are looking for objects based not only on their attributes but also their position i.e. their relationship to other objects, in the object graph.

Let me copy and paste some very important advice: Core Data is not SQL. Entities are not tables. Objects are not rows. Columns are not attributes. Core Data is an object graph management system that may or may not persist the object graph and may or may not use SQL far behind the scenes to do so. Trying to think of Core Data in SQL terms will cause you to completely misunderstand Core Data and result in much grief and wasted time.

It's natural to try and program a new API using the designs of an API you are already familiar with but it is a dangerous trap when the new API has a substantially different design philosophy from the old API.

If you find yourself trying to write a basic and fundemental function of the old API in the new one, that alone should warn you that you are not in sync with the new APIs philosophy. In this case, you should be asking why if a generic findByAttribute method was useful in Core Data, why didn't Apple supply one? Isn't more likely you've missed an important concept in Core Data?

like image 143
TechZen Avatar answered Nov 15 '22 14:11

TechZen