How to use basic data for dependency injection

I use Core Data to manage the schedule of objects, mainly for dependency injection (a subset of NSManagedObjects needs to be preserved, but this is not the subject of my question). When doing unit tests, I want to take on the creation of NSManagedObjects, replacing them with mocks.

At the moment, I have a candidate to use this to use the runtime method_exchangeImplementations to exchange [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:] with my own implementation (i.e. returning mocks). This works for the little test I did.

I have two questions regarding this:

  • Is there a better way to replace creating Data Data Data objects than swizzling insertNewObjectForEntityForName: inManagedObjectContext? I did not go far at runtime or Core Data, and something obvious may be missing.
  • My concept of creating a replacement object is to return mocking NSManagedObjects. I use OCMock, which will not directly mock NSManagedObject subclasses due to their dynamic @property s. So far, my NSManagedObject clients are talking to protocols, not specific objects, so I am returning error protocols, not specific objects. Is there a better way?

Here is some kind of pseudo code to illustrate what I get. Here is a class that I can test:

 @interface ClassUnderTest : NSObject - (id) initWithAnObject:(Thingy *)anObject anotherObject:(Thingo *)anotherObject; @end @interface ClassUnderTest() @property (strong, nonatomic, readonly) Thingy *myThingy; @property (strong, nonatomic, readonly) Thingo *myThingo; @end @implementation ClassUnderTest @synthesize myThingy = _myThingy, myThingo = _myThingo; - (id) initWithAnObject:(Thingy *)anObject anotherObject:(Thingo *)anotherObject { if((self = [super init])) { _myThingy = anObject; _myThingo = anotherObject; } return self; } @end 

I decided to subclass Thingy and Thingo NSManagedObject, perhaps for saving, etc., but I can also replace init with something like:

 @interface ClassUnderTest : NSObject - (id) initWithManageObjectContext:(NSManagedObjectContext *)context; @end @implementation ClassUnderTest @synthesize myThingy = managedObjectContext= _managedObjectContext, _myThingy, myThingo = _myThingo; - (id) initWithManageObjectContext:(NSManagedObjectContext *)context { if((self = [super init])) { _managedObjectContext = context; _myThingy = [NSEntityDescription insertNewObjectForEntityForName:@"Thingy" inManagedObjectContext:context]; _myThingo = [NSEntityDescription insertNewObjectForEntityForName:@"Thingo" inManagedObjectContext:context]; } return self; } @end 

Then in my unit tests I can do something like:

 - (void)setUp { Class entityDescrClass = [NSEntityDescription class]; Method originalMethod = class_getClassMethod(entityDescrClass, @selector(insertNewObjectForEntityForName:inManagedObjectContext:)); Method newMethod = class_getClassMethod([FakeEntityDescription class], @selector(insertNewObjectForEntityForName:inManagedObjectContext:)); method_exchangeImplementations(originalMethod, newMethod); } 

... where my []FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext] returns mocks instead of real NSManagedObjects (or the protocols they implement). The sole purpose of these layouts is to test the calls made by him during unit testing of ClassUnderTest. All return values ​​will be truncated (including any getters related to other NSManagedObjects).

My test instances of ClassUnderTest will be created as part of unit tests, this way:

ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];

(the context will not actually be used in the test due to my swizzled insertNewObjectForEntityForName:inManagedObjectContext )

The point of all this? In any case, I will use Core Data for many classes, so I could use it to reduce the burden of managing changes in the constructors (each change in the constructor includes editing all clients, including a bunch of unit tests). If I had not used Core Data, I could consider something like Objection .

+4
source share
4 answers

Looking at your sample code, it seems to me that your test is bogged down in the details of the base data API, and as a result, the test is not easy to decrypt. All you care about is that the CD object was created. What I would recommend is abstracting the data from the CD. A few ideas:

1) Create instance methods in ClassUnderTest that complete your CD objects and mock them:

 ClassUnderTest *thingyMaker = [ClassUnderTest alloc]; id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker]; [[[mockThingyMaker expect] andReturn:mockThingy] createThingy]; thingyMaker = [thingyMaker initWithContext:nil]; assertThat([thingyMaker thingy], sameInstance(mockThingy)); 

2) Create a convenience method in the superclass ClassUnderTest, for example -(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; . Then you can make fun of the calls to this method using a partial layout:

 ClassUnderTest *thingyMaker = [ClassUnderTest alloc]; id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker]; [[[mockThingyMaker expect] andReturn:mockThingy] createManagedObjectOfType:@"Thingy" inContext:[OCMArg any]]; thingyMaker = [thingyMaker initWithContext:nil]; assertThat([thingyMaker thingy], sameInstance(mockThingy)); 

3) Create a helper class that handles common CD tasks and makes fun of the calls of this class. I use this class in some of my projects:

 @interface CoreDataHelper : NSObject {} +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate; +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate sortedBy:(NSArray *)sortDescriptors; +(NSArray *)findManagedObjectsOfType:(NSString *)type inContext:(NSManagedObjectContext *)context usingPredicate:(NSPredicate *)predicate sortedBy:(NSArray *)sortDescriptors limit:(int)limit; +(NSManagedObject *)findManagedObjectByID:(NSString *)objectID inContext:(NSManagedObjectContext *)context; +(NSString *)coreDataIDForManagedObject:(NSManagedObject *)object; +(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; @end 

It's harder to taunt, but you can check out my blog post mocking class methods for a relatively simple approach.

+1
source

I find that there are usually 2 types of tests that include Core Data objects: 1) test methods that take the object as an argument, and 2) test methods that actually control the CRUD operations for the underlying data objects.

For # 1, I do what sounds like you do, as @ graham-lee recommends : create a protocol for your objects, and make fun of that protocol in your tests. I don’t see how he adds additional code - you can define properties in the protocol and correspond to the entity class protocol:

 @protocol CategoryInterface <NSObject> @property(nonatomic,retain) NSString *label; @property(nonatomic,retain) NSSet *items; @property(nonatomic,retain) NSNumber *position; @end @interface Category : NSManagedObject<CategoryInterface> {} @end 

As for No. 2, I usually set up in-memory storage in my unit tests and just testing functional tests using in-memory storage.

 static NSManagedObjectModel *model; static NSPersistentStoreCoordinator *coordinator; static NSManagedObjectContext *context; static NSPersistentStore *store; CategoryManager *categoryManager; -(void)setUp { [super setUp]; // set up the store NSString *userPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"category" ofType:@"momd"]; NSURL *userMomdURL = [NSURL fileURLWithPath:userPath]; model = [[NSManagedObjectModel alloc] initWithContentsOfURL:userMomdURL]; coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; store = [coordinator addPersistentStoreWithType: NSInMemoryStoreType configuration: nil URL: nil options: nil error: NULL]; context = [[NSManagedObjectContext alloc] init]; // set the context on the manager [context setPersistentStoreCoordinator:coordinator]; [categoryManager setContext:context]; } -(void)tearDown { assertThat(coordinator, isNot(nil)); assertThat(model, isNot(nil)); NSError *error; STAssertTrue([coordinator removePersistentStore:store error:&error], @"couldn't remove persistent store: %@", [error userInfo]); [super tearDown]; } 

I check in tearDown that the coordinator and model were successfully created because I found that there were times when the creation setUp exception in setUp , so the tests did not actually run. This will cause such a problem.

+3
source

Here is a blog post about it: http://iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html

There is a tutorial video on ideveloper.tv that mentions how to do unit testing on many cocoa frames, including coredata: http://ideveloper.tv/store/details?product_code=10007

+2
source

I don't like mocks for Core Data because the object graph and the managed objects themselves can be complex to simulate accurately. Instead, I prefer to generate a full link storage file and validate it. This works more, but the results are better.

Update:

is there a better way to replace creating Data Data Data objects than swizzling insertNewObjectForEntityForName: inManagedObjectContext?

If you want to just test the class, i.e. separate instance in isolation, then you don’t need to insert the object into the context at all. Instead, you can simply initialize it like any other object. Accessories and other methods will work as usual, but there is simply no context that observes changes and "controls" the relationship of an object with other "managed" objects.

my concept of creating a replacement object is to return the bullying of NSManagedObjects. I use OCMock, which will not directly mock NSManagedObject subclasses due to their dynamic @propertys. For now, my NSManagedObject clients are talking to protocols, not specific objects, so I'm returning wrong protocols, not specific objects. Is there a better way?

It depends on what you are actually experiencing. If you are testing the NSManagedObject subclass itself, then the protocol layout is useless. If you test other classes that interact with or manage a managed object, then the mock protocol will work fine.

The import thing to understand when testing Core Data is that the complex complexity of Core Data arises when plotting an object at runtime. Retrieving and setting attributes is trivial; this interconnection and monitoring of key values ​​is becoming more difficult. You really cannot mock the latter with any precision, so I recommend creating a graph of the reference object for testing.

+1
source

All Articles