问题
I'm toying with using Core Data to manage a graph of objects, mainly for dependency injection (a subset of the NSManagedObjects do need to be persisted, but that isn't the focus of my question). When running unit tests, I want to take over creation of the NSManagedObjects, replacing them with mocks.
I do have a candidate means of doing this for now, which is to use the runtime's method_exchangeImplementations to exchange [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:]
with my own implementation (ie. returning mocks). This works for a small test I've done.
I have two questions regarding this:
- is there a better way to replace Core Data's object creation than swizzling insertNewObjectForEntityForName:inManagedObjectContext? I haven't forayed far into the runtime or Core Data, and may be missing something obvious.
- my replacement object creation method concept is to return mocked NSManagedObjects. I'm using OCMock, which won't directly mock NSManagedObject subclasses because of their dynamic
@property
s. For now my NSManagedObject's clients are talking to protocols rather than concrete objects, so I return mocked protocols rather than concrete objects. Is there a better way?
Here's some pseudoish code to illustrate what I'm getting at. Here's a class I might be testing:
@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 decide to make Thingy and Thingo NSManagedObject subclasses, perhaps for persistence etc, but also so I can replace the 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 in place of real NSManagedObjects (or protocols they implement). The only purpose of these mocks is to verify calls made to them while unit-testing ClassUnderTest. All return values will be stubbed (including any getters referring to other NSManagedObjects).
My test ClassUnderTest
instances will be created within the unit tests thus:
ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];
(the context won't actually be used in test, because of my swizzled insertNewObjectForEntityForName:inManagedObjectContext
)
The point of all this? I'm going to be using Core Data for many of the classes anyway, so I might as well use it to help reduce the burden managing changes in constructors (every constructor change involves editing all clients including a bunch of unit tests). If I wasn't using Core Data, I might consider something like Objection.
回答1:
Looking at your sample code, it seems to me your test is getting bogged down in the details of the Core Data API, and as a result the test isn't easy to decipher. All you care about is that a CD object was created. What I'd recommend is abstracting away the CD details. A few ideas:
1) Create instance methods in ClassUnderTest that wrap the creation of 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 ClassUnderTest's superclass, like -(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;
. Then you can mock calls to that method using a partial mock:
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 mock the calls to that class. I use a class like this 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
These are trickier to mock, but you can check out my blog post on mocking class methods for a relatively straightforward approach.
回答2:
I find that there are generally 2 types of tests that involve Core Data entities: 1) testing methods that take an entity as an argument, and 2) testing methods that actually manage CRUD operations on the core data entities.
For #1, I do what it sounds like you're doing, as @graham-lee recommends: create a protocol for your entities, and mock that protocol in your tests. I don't see how it adds any extra code--you can define the properties in the protocol and have the entity class conform to the 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 to #2, I generally set up an in-memory store in my unit tests and just test do functional tests using an in-memory store.
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 verify in tearDown
that the coordinator and model were successfully created, because I found that there were times when the creation threw an exception in setUp
, so the tests weren't actually running. This will catch that kind of problem.
回答3:
Here is a blog posting on this: http://iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html
There is a training video on the ideveloper.tv site that mentions how to do unit testing in many of the cocoa frameworks including coredata: http://ideveloper.tv/store/details?product_code=10007
回答4:
I dislike mocks for Core Data because the object graph and the managed objects themselves can be to complex to accurately mock. Instead, I prefer to generate a full fledge reference store file and test against that. It's more work but the results are better.
Update:
is there a better way to replace Core Data's object creation than swizzling insertNewObjectForEntityForName:inManagedObjectContext?
If you just want to test the class i.e. a single instance in isolation, then you don't have to insert the object into a context at all. Instead, you can just initialize it like any other object. The accessors and other methods will work as normal but there is simply no context observing the changes and "managing" the object's relationship to other "managed" objects.
my replacement object creation method concept is to return mocked NSManagedObjects. I'm using OCMock, which won't directly mock NSManagedObject subclasses because of their dynamic @propertys. For now my NSManagedObject's clients are talking to protocols rather than concrete objects, so I return mocked protocols rather than concrete objects. Is there a better way?
It depends on what you are actually testing. If you are testing the NSManagedObject-subclass itself, then the mock protocol is useless. If you are testing other classes that communicate with or manipulate the managed object then the mock protocol will work fine.
The import thing to grasp when testing Core Data is that the tricky complexity in Core Data comes in the construction of the object graph at runtime. The getting and setting of attributes is trivial, it is the relationships and the key-value observing that gets complicated. You really can't mock the latter with any accuracy which is why I recommend creating a reference object graph to test against.
来源:https://stackoverflow.com/questions/6880739/how-to-use-core-data-for-dependency-injection