Reputation: 16176
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
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
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
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