4

我正在玩弄使用 Core Data 来管理对象图,主要用于依赖注入(NSManagedObjects 的一个子集确实需要持久化,但这不是我问题的重点)。运行单元测试时,我想接管 NSManagedObjects 的创建,用模拟替换它们。

我现在确实有一种候选方法,即使用运行时的 method_exchangeImplementations[NSEntityDescription insertNewObjectForEntityForName:inManagedObjectContext:]与我自己的实现进行交换(即返回模拟)。这适用于我所做的一个小测试。

我对此有两个问题:

  1. 有没有比 swizzling insertNewObjectForEntityForName:inManagedObjectContext 更好的方法来替换 Core Data 的对象创建?我还没有深入研究运行时或核心数据,可能会遗漏一些明显的东西。
  2. 我的替换对象创建方法概念是返回模拟的 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:@"Thingy" inManagedObjectContext:context];
        _myThingo = [NSEntityDescription insertNewObjectForEntityForName:@"Thingo" inManagedObjectContext:context];
    }

    return self;
}
@end

然后在我的单元测试中,我可以执行以下操作:

- (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]返回模拟代替了真正的 NSManagedObjects(或它们实现的协议)。这些模拟的唯一目的是在对 ClassUnderTest 进行单元测试时验证对它们的调用。所有返回值都将被存根(包括任何引用其他 NSManagedObjects 的 getter)。

我的测试ClassUnderTest实例将在单元测试中创建,因此:

ClassUnderTest *testObject = [ClassUnderTest initWithManagedObjectContext:mockContext];

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

这一切的重点?无论如何,我将在许多类中使用 Core Data,所以我不妨使用它来帮助减轻管理构造函数更改的负担(每个构造函数更改都涉及编辑所有客户端,包括一堆单元测试)。如果我没有使用 Core Data,我可能会考虑使用Objection 之类的东西。

4

4 回答 4

3

我发现通常有两种类型的测试涉及核心数据实体:1)以实体作为参数的测试方法,以及 2)实际管理核心数据实体上的 CRUD 操作的测试方法。

对于#1,我会按照@graham-lee 的建议做你正在做的事情:为你的实体创建一个协议,并在你的测试中模拟该协议。我看不出它是如何添加任何额外代码的——您可以在协议中定义属性并使实体类符合协议:

@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,我通常在我的单元测试中设置一个内存存储,并使用内存存储进行功能测试。

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了协调器和模型是否已成功创建,因为我发现有时创建时会引发异常setUp,因此测试实际上并未运行。这将解决这类问题。

于 2011-07-31T05:35:50.043 回答
2

这是一篇关于此的博客文章:http: //iamleeg.blogspot.com/2009/09/unit-testing-core-data-driven-apps.html

ideveloper.tv 网站上有一个培训视频,其中提到了如何在包括 coredata 在内的许多可可框架中进行单元测试:http://ideveloper.tv/store/details?product_code= 10007

于 2011-07-30T12:49:22.703 回答
1

我不喜欢 Core Data 的模拟,因为对象图和托管对象本身可能很复杂,无法准确模拟。相反,我更喜欢生成一个完整的参考存储文件并针对它进行测试。这是更多的工作,但结果更好。

更新:

有没有比 swizzling insertNewObjectForEntityForName:inManagedObjectContext 更好的方法来替换 Core Data 的对象创建?

如果您只想单独测试该类,即单个实例,那么您根本不必将对象插入到上下文中。相反,您可以像任何其他对象一样初始化它。访问器和其他方法将正常工作,但根本没有上下文观察更改和“管理”对象与其他“托管”对象的关系。

我的替换对象创建方法概念是返回模拟的 NSManagedObjects。我正在使用 OCMock,它不会直接模拟 NSManagedObject 子类,因为它们是动态的@propertys。现在我的 NSManagedObject 的客户正在与协议而不是具体对象交谈,所以我返回模拟协议而不是具体对象。有没有更好的办法?

这取决于您实际测试的内容。如果您正在测试 NSManagedObject 子类本身,那么模拟协议是无用的。如果您正在测试与托管对象通信或操作托管对象的其他类,那么模拟协议将正常工作。

测试 Core Data 时要掌握的重要一点是,Core Data 的棘手复杂性在于运行时对象图的构建。属性的获取和设置是微不足道的,复杂的是关系和键值观察。您真的无法准确地模拟后者,这就是为什么我建议创建一个参考对象图来进行测试的原因。

于 2011-07-30T14:16:46.800 回答
1

查看您的示例代码,在我看来,您的测试陷入了 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:@"Thingy" inContext:[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:(int)limit;
+(NSManagedObject *)findManagedObjectByID:(NSString *)objectID inContext:(NSManagedObjectContext *)context;
+(NSString *)coreDataIDForManagedObject:(NSManagedObject *)object;
+(NSManagedObject *)createManagedObjectOfType:(NSString *)type inContext:(NSManagedObjectContext *)context;    

@end

这些模拟起来比较棘手,但是您可以查看我关于模拟类方法的博客文章,以获得相对简单的方法。

于 2011-08-04T05:25:17.560 回答