用OCMock嘲笑KVO

时间:2012-03-07 14:33:26

标签: objective-c cocoa unit-testing key-value-observing ocmock

我想测试Key-Value-Observation是否适用于我的一类。它有一个属性,这取决于另一个属性。它们的设置如下:

+ (NSSet *)keyPathsForValuesAffectingSecondProperty {
    return [NSSet setWithObjects:
            @"firstProperty",
            nil];
}

- (NSArray *)secondProperty {
    return [self.firstProperty array];
}

我想运行单元测试来验证当firstProperty更改时,绑定到secondProperty的对象会收到通知。起初我以为我可以使用+[OCMockObject observerMock],但看起来只能与NSNotificationCenter一起使用。测试这个的最佳方法是什么?

3 个答案:

答案 0 :(得分:7)

在@ chrispix的回答激励我朝着不同的方向努力之后,我对此工作了一段时间。我从这开始:

id objectToObserve = [[TheClassBeingTested alloc] init];

id secondPropertyObserver = [OCMockObject mockForClass:[NSObject class]];
[[secondPropertyObserver expect] observeValueForKeyPath:@"secondProperty"
                                               ofObject:objectToObserve
                                                 change:OCMOCK_ANY
                                                context:[OCMArg anyPointer]];
[objectToObserve addObserver:secondPropertyObserver
                  forKeyPath:@"secondProperty"
                     options:NSKeyValueObservingOptionNew
                     context:NULL];

// Do something to modify objectToObserve's firstProperty    

[secondPropertyObserver verify];

当我运行此测试代码时,我收到以下消息:

OCMockObject[NSObject]: unexpected method invoked: isKindOfClass:<??> 
    expected:     observeValueForKeyPath:@"firstProperty" ofObject:

我做了一些调查,发现模拟对象调用的-isKindOfClass:调用没有传递给NSKeyValueObservance类对象。

我尝试添加以下代码来模拟响应,但YESNO的值都失败,并且堆栈中存在NSKeyValueWillChange的EXC_BAD_ACCESS异常。

BOOL returnVal = NO;
[[[secondPropertyObserver stub] andReturnValue:OCMOCK_VALUE(returnVal)] isKindOfClass:[OCMArg any]];

我更谨慎地走了一步,发现我的代码没有导致这个异常 - 就在autoreleasepool被排空的时候。然后我突然意识到我需要移除观察者。以下是完整的解决方案,包括删除观察者。

id objectToObserve = [[TheClassBeingTested alloc] init];

id secondPropertyObserver = [OCMockObject mockForClass:[NSObject class]];

BOOL returnVal = NO;
[[[secondPropertyObserver stub] andReturnValue:OCMOCK_VALUE(returnVal)] isKindOfClass:[OCMArg any]];

[[secondPropertyObserver expect] observeValueForKeyPath:@"secondProperty"
                                               ofObject:objectToObserve
                                                 change:OCMOCK_ANY
                                                context:[OCMArg anyPointer]];

[objectToObserve addObserver:secondPropertyObserver
                  forKeyPath:@"secondProperty"
                     options:NSKeyValueObservingOptionNew
                     context:NULL];

// Do something to modify objectToObserve's firstProperty    

[secondPropertyObserver verify];

[objectToObserve removeObserver:secondPropertyObserver
                     forKeyPath:@"secondProperty"];

答案 1 :(得分:2)

如果secondProperty收到您可以验证的通知有一些结果,我只需将其设置为实际对象并在之后验证状态。如果那是不可能的,你可以创建secondProperty的部分模拟并期望KVO回调:

id observedObject = // ...initialize observed class
observedObject.secondProperty = // initialize secondProperty
id mockSecondProperty = [OCMockObject partialMockForObject:observedObject.secondProperty];
[[mockSecondProperty expect] observeValueForKeyPath:@"firstProperty" ofObject:observedObject change:OCMOCK_ANY context:OCMOCK_ANY];

observedObject.firstProperty = // modify firstProperty

[mockSecondProperty verify];

答案 2 :(得分:1)

仅适用于对此感兴趣的人:这是没有OCMock的解决方案。

我只是将我期望更改的值绑定到我的单元测试类的测试属性。

在以下示例中,我希望更新numberOfPages属性,并在更改pages数组时触发KVO通知。

我在boundInteger课程中声明了DocumentTests属性:

@interface DocumentTests : SenTestCase 
@property (assign) int boundInteger;
@end

然后我实现我的测试用例:

- (void)testNumberOfPages
{
    Document *doc = [[Document alloc] init];

    [self bind:@"boundInteger" toObject:doc withKeyPath:@"numberOfPages" options:nil];

    doc.pages = arrayOfThreePagesIHaveBuildSomewhereElse;

    STAssertEquals(self.boundInteger, 3, @"Wrong number of pages.");
    STAssertEquals(doc.numberOfPages, 3, @"Wrong number of pages.");

    [self unbind:@"boundInteger"];
}

适合我的工作:)