如何使用Core Data进行依赖注入 [英] How to use Core Data for Dependency Injection
问题描述
我使用Core Data来管理对象图,主要是依赖注入(NSManagedObject的一个子集需要持久化,但这不是我的问题的焦点)。当运行单元测试时,我想接管NSManagedObjects的创建,用mock替换它们。
我现在有一个候选方法,使用运行时的method_exchangeImplementations用我自己的实现(即返回mocks)来交换 [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:]
。
我有两个问题:
- 是否有更好的方法来替换Core Data的对象创建比swizzling insertNewObjectForEntityForName:inManagedObjectContext?我没有深入到运行时或Core Data,可能会丢失一些明显的东西。
- 我的替换对象创建方法的概念是返回mocked NSManagedObjects。我使用OCMock,它不会直接模拟NSManagedObject子类,因为它们的动态
@property
。现在我的NSManagedObject的客户端正在谈论协议而不是具体对象,所以我返回模拟协议而不是具体对象。是否有更好的方法?
这里有一些伪代码来说明我在做什么。这里是一个我可能会测试的类:
@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
我决定使Thingy和Thingo NSManagedObject子类,也许是为了持久化等,但也可以用如下替换init:
@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:@ThingyinManagedObjectContext:context];
_myThingo = [NSEntityDescription insertNewObjectForEntityForName:@ThingoinManagedObjectContext:context];
}
return self;
}
@end
然后在我的单元测试中,我可以做:
- (void)setUp {
Class entityDescrClass = [NSEntityDescription class];
方法originalMethod = class_getClassMethod(entityDescrClass,@selector(insertNewObjectForEntityForName:inManagedObjectContext :));
方法newMethod = class_getClassMethod([FakeEntityDescription class],@selector(insertNewObjectForEntityForName:inManagedObjectContext :));
method_exchangeImplementations(originalMethod,newMethod);
}
...其中my ] FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext]
返回mocks代替真正的NSManagedObjects(或它们实现的协议)。这些mock的 only 目的是验证在对UnitUnderTest进行单元测试时对它们的调用。所有返回值都将被存根(包括任何引用其他NSManagedObject的getter)。
我的测试 ClassUnderTest
在单元测试中创建如下:
ClassUnderTest * testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];
(上下文实际上不会在测试中使用,因为我调整了 insertNewObjectForEntityForName:inManagedObjectContext
)
这一切的点?我将使用Core Data的许多类,所以我也可以使用它来帮助减少管理构造函数的变化(每个构造函数更改涉及编辑所有客户端,包括一堆单元测试)。如果我不使用Core Data,我可能会考虑类似异议。
看看你的示例代码,在我看来,你的测试陷入了核心数据API的细节,因此测试不容易解密。所有你关心的是一个CD对象被创建。我推荐的是抽象出CD的细节。几个想法:
1)在ClassUnderTest中创建实例方法,包装您的CD对象的创建,并模拟它们:
ClassUnderTest * thingyMaker = [ClassUnderTest alloc];
id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker];
[[[mockThingyMaker expect] andReturn:mockThingy] createThingy];
thingyMaker = [thingyMaker initWithContext:nil];
assertThat([thingyMaker thingy],sameInstance(mockThingy)); 2)在ClassUnderTest的超类中创建一个方便的方法,例如 - (NSManagedObject *),它将返回一个方便的方法:
< )createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; 。然后你可以使用部分模拟来模拟对该方法的调用:
ClassUnderTest * thingyMaker = [ClassUnderTest alloc];
id mockThingyMaker = [OCMockObject partialMockForObject:thingyMaker];
[[[mockThingyMaker expect] andReturn:mockThingy] createManagedObjectOfType:@ThingyinContext:[OCMArg any]];
thingyMaker = [thingyMaker initWithContext:nil];
assertThat([thingyMaker thingy],sameInstance(mockThingy));
3)创建一个帮助器类来处理常见的CD任务,并模拟对该类的调用。我在我的一些项目中使用类似这样的类:
@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:
+(NSManagedObject *)findManagedObjectByID:(NSString *)objectID inContext:(NSManagedObjectContext *)context;
+(NSString *)coreDataIDForManagedObject:(NSManagedObject *)object;
+(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;
@end
这些都很难模拟,但你可以查看我的博客文章嘲讽类方法的相对直接的方法。 / p>
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.
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.
这篇关于如何使用Core Data进行依赖注入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!