如何使用Core Data进行依赖注入 [英] How to use Core Data for Dependency Injection

查看:185
本文介绍了如何使用Core Data进行依赖注入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用Core Data来管理对象图,主要是依赖注入(NSManagedObject的一个子集需要持久化,但这不是我的问题的焦点)。当运行单元测试时,我想接管NSManagedObjects的创建,用mock替换它们。



我现在有一个候选方法,使用运行时的method_exchangeImplementations用我自己的实现(即返回mocks)来交换 [NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:]



我有两个问题:


  1. 是否有更好的方法来替换Core Data的对象创建比swizzling insertNewObjectForEntityForName:inManagedObjectContext?我没有深入到运行时或Core Data,可能会丢失一些明显的东西。

  2. 我的替换对象创建方法的概念是返回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:

  1. 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.
  2. 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?

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆