如何使用核心数据进行dependency injection

我玩弄使用核心数据来pipe理对象的graphics,主要是为了dependency injection(NSManagedObjects的一个子集需要被持久化,但这不是我的问题的焦点)。 当运行unit testing时,我想要接pipeNSManagedObjects的创build,用mockreplace它们。

我现在有一个候选的方法,就是使用运行时的method_exchangeImplementations来交换[NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:]与我自己的实现(即返回模拟)。 这适用于我已经完成的一个小testing。

我有两个问题:

  1. 有没有更好的方式来replace核心数据的对象创build比swizzling insertNewObjectForEntityForName:inManagedObjectContext? 我没有进入运行时或核心数据,可能会漏掉一些明显的东西。
  2. 我的replace对象创build方法的概念是返回嘲笑的NSManagedObjects。 我使用的OCMock,它不会直接模拟NSManagedObject子类,因为它们的dynamic@property s。 现在我的NSManagedObject的客户端正在与协议而不是具体的对象交stream,所以我返回了嘲讽的协议而不是具体的对象。 有没有更好的办法?

这里有一些伪装的代码来说明我所得到的。 这是我可能要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 

我决定使Thingy和Thingo NSManagedObject子类,也许为持久性等,但也可以用类似的东西replaceinit:

 @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 

然后在我的unit testing中,我可以做一些事情:

 - (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); } 

…我的[]FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext]返回[]FakeEntityDescription insertNewObjectForEntityForName:inManagedObjectContext]代替真正的NSManagedObjects(或他们实现的协议)。 这些模拟的唯一目的是在unit testingClassUnderTest时validation对它们的调用。 所有的返回值将被存根(包括引用其他NSManagedObjects的任何getter)。

我的testingClassUnderTest实例将在unit testing中创build,因此:

ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];

(上下文实际上不会在testing中使用,因为我的swizzled insertNewObjectForEntityForName:inManagedObjectContext

这一切的重点? 无论如何,我将会为许多类使用Core Data,所以我不妨使用它来帮助减轻pipe理构造函数更改的负担(每个构造函数更改都涉及到编辑所有客户端,包括一系列unit testing)。 如果我不使用核心数据,我可能会考虑类似异议 。

看看你的示例代码,在我看来,你的testing陷入了核心数据API的细节,因此testing不容易破译。 你所关心的只是一个CD对象的创build。 我推荐的是摘录CD的细节。 一些想法:

1)在ClassUnderTest中创build包装你的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的超类中创build一个方便的方法,如-(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; 。 然后你可以使用部分模拟模拟对这个方法的调用:

 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)创build一个处理常见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:(int)limit; +(NSManagedObject *)findManagedObjectByID:(NSString *)objectID inContext:(NSManagedObjectContext *)context; +(NSString *)coreDataIDForManagedObject:(NSManagedObject *)object; +(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context; @end 

这些嘲讽更棘手,但你可以看看我的博客文章嘲笑类方法相对简单的方法。

我发现通常有两类涉及核心数据实体的testing:1)以实体为参数的testing方法; 2)实际上在核心数据实体上pipe理CRUD操作的testing方法。

对于#1,我听起来像是在做什么,因为@ graham-lee build议 :为您的实体创build协议,并在您的testing中嘲笑该协议。 我不明白它是如何增加任何额外的代码 – 你可以在协议中定义属性,并使实体类符合协议:

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

至于#2,我通常在我的unit testing中build立一个内存存储,并使用内存存储来testingfunctiontesting。

 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]; } 

我在tearDown中validation协调器和模型是成功创build的,因为我发现有时创build在setUp抛出exception,所以testing并没有真正运行。 这将会遇到这样的问题。

这里是一个博客张贴在这: http : //iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html

在ideveloper.tv网站上有一个培训video,其中提到了如何在许多cocoa框架(包括coredata)中进行unit testing: http ://ideveloper.tv/store/details?product_code=10007

我不喜欢模拟核心数据,因为对象图和pipe理对象本身可以复杂到精确模拟。 相反,我更愿意生成一个完整的Fledge参考存储文件并进行testing。 这是更多的工作,但结果是更好的。

更新:

有没有更好的方式来replace核心数据的对象创build比swizzling insertNewObjectForEntityForName:inManagedObjectContext?

如果你只想testing这个类,也就是单独的一个实例,那么你就不必把对象插入到上下文中了。 相反,你可以像其他任何对象一样初始化它。 访问器和其他方法将正常工作,但没有上下文观察变化,并“pipe理”对象与其他“被pipe理”对象的关系。

我的replace对象创build方法的概念是返回嘲笑的NSManagedObjects。 我使用的OCMock,它不会直接模拟NSManagedObject子类,因为它们的dynamic@propertys。 现在我的NSManagedObject的客户端正在与协议而不是具体的对象交stream,所以我返回了嘲讽的协议而不是具体的对象。 有没有更好的办法?

这取决于你正在testing什么。 如果你正在testingNSManagedObject子类本身,那么模拟协议是没有用的。 如果您正在testing其他类与通信或操纵pipe理对象,那么模拟协议将正常工作。

testing核心数据时需要掌握的重要事情是,核心数据中棘手的复杂性来自运行时对象图的构build。 属性的获取和设置是微不足道的,关系和关键值观察变得复杂。 你真的不能嘲笑后者的任何准确性,这就是为什么我build议创build一个参考对象图来testing。