使用 OCMock 和 MagicalRecord 进行单元测试

Posted

技术标签:

【中文标题】使用 OCMock 和 MagicalRecord 进行单元测试【英文标题】:Unit testing with OCMock and MagicalRecord 【发布时间】:2012-10-11 22:23:02 【问题描述】:

虽然我在 SO 中看到过类似的问题,但似乎没有一个答案能解决我的问题。

我有一个由 mogenerator 生成的带有自定义函数的 NSManagedObject 类(不在模型中):

@interface MyManagedClass : _MyManagedClass 
-(NSNumber*)getRandomNumber2;
-(void)function_I_want_to_test;

我的 function_I_want_to_test() 取决于 random() 的结果,这是我在测试期间必须控制的。由于我无法模拟 random(),所以我构建了一个函数包装器,顺便说一下,它不是静态的,因为 OCMock 和静态类函数有很多问题。

我的单元测试设置如下:

[MagicalRecord setDefaultModelFromClass:[self class]];
[MagicalRecord setupCoreDataStackWithInMemoryStore];

使用调试器,我可以验证模型是否已正确加载。另外,如果我以非 magical 方式进行操作:

    NSBundle *b = [NSBundle bundleForClass:[self class]];
    model = [NSManagedObjectModel mergedModelFromBundles:@[b]];

在这一点之后,我无法创建任何模拟来存根我的 random() 包装函数

我试过模拟类

id mock = [OCMockObject mockForClass:[MyManagedClass class]];
[[[mock stub] andReturn:@50] getRandomNumber2];
MyManagedClass *my_object = [mock MR_createEntity];

我尝试过使用部分模拟

MyManagedClass *my_object = [MyManagedClass MR_createEntity];
id mock2 = [OCMockObject partialMockForObject:my_object];

在最后一点之后,仅仅创建一个mock2的实例就破坏了my_object的动态属性,它变得无用了。

我也尝试过使用模拟协议和我想要存根的函数,但仍然无济于事。

运行时异常是其他人在对 Core Data 对象进行测试时遇到的正常情况:属性不是可识别的选择器。

但对我来说奇怪的是,我并没有尝试存根任何动态属性,而是一个正常的、编译时已知的函数。因此,我觉得使用 OCMock 会使我的实例毫无用处似乎很奇怪。

理想情况下,我想要使用 OCMock/Mogenerator/Magicalrecord 的东西。

我做错了什么?

【问题讨论】:

【参考方案1】:

我建议不要尝试模拟托管对象。为了使托管对象工作,有很多运行时的疯狂。这就是为什么我建议使用内存数据库方法进行测试。这将允许您创建实体的空实例,同时让核心数据发生。

由于您可能正在使用单元测试,我建议在您认为需要模拟一些数据来重新创建整个堆栈的每个测试用例中,并设置一个具有运行您所需的状态的新实体测试。您还可以在内存持久存储中进行测试,与默认堆栈创建方法为您提供的测试分开,并将第二个测试附加到您的默认堆栈。也就是说,创建一个新的内存存储,使用您的假/模拟数据实体对其进行初始化,并将其附加到您的测试数据堆栈。

我希望这个漫无边际的内容能有所帮助,但最重要的是不要模拟托管对象......真的。

【讨论】:

这当然不是我想听到的答案,但是在使用 OCmock 一段时间并看到它在托管对象上失败后,我什至有过重新考虑完全放弃 Core Data 的时刻。但是我正在开发一个新的程序来规避一些问题。例如,实例函数很容易用存储块对象的变量替换。像我的内部随机()这样的“关键”函数也可以是像_Class_random()这样的全局块变量。它可能非常脏,但对程序的影响最小。【参考方案2】:

您可以通过将您的随机数生成移出核心数据对象并进入辅助类来实现:

@implementation RandomNumberGenerator

static RandomNumberGenerator *_sharedInstance = nil;

+(RandomNumberGenerator *)sharedInstance 
    if (_sharedInstance == nil) 
        _sharedInstance = [[RandomNumberGenerator alloc] init];
        
    return _sharedInstance;


+(void)setSharedInstance:(RandomNumberGenerator *)instance 
    [instance retain];
    [_sharedInstance release];
    _sharedInstance = instance;    


-(NSNumber *)generateRandomNumber 
    return ...


@end

然后在你的测试中:

-(void)testSomething 
    id mockGenerator = [OCMockObject mockForClass:[RandomNumberGenerator class]];
    [RandomNumberGenerator setSharedInstance:mockGenerator];
    [[[mockGenerator stub] andReturn:@(4)] generateRandomNumber];

    MyManagedClass *my_object = [MyManagedClass MR_createEntity];

    expect(my_object.someField).to.equal(someValueBasedOnGeneratedRandomNumber);

【讨论】:

是的,你也可以这样做。这可能更容易。这将取决于你的情况。但我的观点是,不要模拟您的托管对象。

以上是关于使用 OCMock 和 MagicalRecord 进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

使用 AFNetworking 和 OCMock 的测试网络

OCMock 和 UIViewController

使用 OCMock 进行 OSX 应用程序测试

OCMock 3 Partial Mock:类方法和 objc 运行时

GHUnit,OCMock:如何测试两个指定块之一是不是被调用?

OCMock:存根 @dynamic 属性