开发者

Set readonly navigationController property on UIViewController for mocking

I have created a mock UINavigationController using OCMock. However, I cannot assign it to the navigationController property of a UIViewController since that property is readonly.

id mockNavController = [OCMockObject mockForClass:[开发者_JS百科UINavigationController class]];
...
myViewController.navigationController = mockNavController; // readonly!

The author of this blog post claims to have found a solution but neglected to share it.


It's not necessary to create a mutator that allows you to set the navigationController property, as you can mock the accessor that returns it. Here's how I do it:

-(void)testTappingSettingsButtonShouldDisplaySettings {
    MyController *myController = [[MyController alloc] init];

    // expect the nav controller to push a settings controller
    id mockNavigationController = [OCMockObject mockForClass:[UINavigationController class]];
    [[mockNavigationController expect] pushViewController:[OCMArg any] animated:YES];

    // set up myController to return the mocked navigation controller
    id mockController = [OCMockObject partialMockForObject:myController];
    [[[mockController expect] andReturn:mockNavigationController] navigationController];

    [myController settingsButtonTapped];

    [mockNavigationController verify];
    [mockController verify];
    [myController release];
}


A very late response, but for posterity I just discovered that an alternative way to do this is to take the state-based approach and actually stick your view controller under test into a real navigation controller. You can then poke at your view controller and test what it does with the nav stack by inspecting the state of the navigation controller. Here's an example:

it(@"displays the station chooser when you tap the 'Choose station' button", ^{
    // Given
    LaunchViewController *launchViewController = [LaunchViewController newWithNearestStationLocator:nil];
    [launchViewController view];
    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:launchViewController];

    // When
    [[launchViewController chooseStationBtn] sendActionsForControlEvents:UIControlEventTouchUpInside];

    // Then
    [[theValue(navController.viewControllers.count) should] equal:theValue(2)];
    [[NSStringFromClass(navController.visibleViewController.class) should] equal:@"StationsViewController"];
});


There are a couple of possible solutions.

You could invoke the private setter for the navigationController but that may not exist or work reliably in all cases.

You could follow Derek's advice and create a category which redefines the navigationController property on UIViewController. Access to the navigationController property should then be safe but if UIViewController accesses the backing ivar directly anywhere and you did not use the same ivar in your category then you might see unexpected behavior.

You could use a partial mock of UINavigationController as in http://blog.carbonfive.com/2010/03/10/testing-view-controllers/. Your test isn't as isolated as you might like in that case but at least the private behavior of your UIViewController superclass and UINavigationController should be unchanged.


One technique I have used in tests is to define a category which adds methods to the main class so that I can access internal properties. You could try using a category to synthesize a setter, bt you may need to know the variable name that holds the navigation controller pointer.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜