Dov
Dov

Reputation: 16176

Mocking KVO with OCMock

I would like to test that Key-Value-Observation is working properly for a class of mine. It has one property, which depends on another. They are set up like so:

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

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

I want to run a unit test to verify that when firstProperty changes, an object bound to secondProperty gets a notification. At first I thought I would be able to use +[OCMockObject observerMock], but it looks like that can only be used with NSNotificationCenter. What would be the best way to test this?

Upvotes: 4

Views: 2769

Answers (3)

Johannes Fahrenkrug
Johannes Fahrenkrug

Reputation: 44808

Just for whoever is interested in this: Here's a solution without OCMock.

I simply bind the value that I expect to change to a test property of my unit test class.

In the following example I want the numberOfPages property to be updated and to fire KVO notifications when I change the pages array.

I declare a boundInteger property on my DocumentTests class:

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

Then I implement my test case like so:

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

Works fine for me :)

Upvotes: 1

Dov
Dov

Reputation: 16176

I worked on this for a while after @chrispix's answer inspired me to work in a different direction. I started with this:

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

When I ran this test code, I got the following message:

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

I did some investigation and found that the -isKindOfClass: call the mock object didn't expect was being passed an NSKeyValueObservance class object.

I tried adding the following code to mock a response, but values of YES and NO both fail with EXC_BAD_ACCESS exceptions with NSKeyValueWillChange in the stack.

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

I stepped more carefully and found that my code wasn't causing this exception - it was while the autoreleasepool was being drained. It then dawned on me that I needed to remove the observer. Below is the complete solution, including removing the observer.

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

Upvotes: 7

Christopher Pickslay
Christopher Pickslay

Reputation: 17772

If there's some outcome to secondProperty receiving the notification that you can verify, I'd just set it to an actual object and validate the state afterwards. If that's not possible, you could create a partial mock of secondProperty and expect the KVO callback:

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

Upvotes: 2

Related Questions