Testing iPhone View Controllers

Posted on by in Mobile, Process

I have been writing tests around my iPhone apps’ view controllers in order to follow the same TDD practices we use in other environments. Writing tests first has changed the way I structure my code in a couple of ways which I think offer immediate and emergent benefits for my applications. Most of an iPhone application’s business logic is implemented in its view controllers. Testing those controllers is therefore a priority if I want to have a well tested application.
Below are some examples of the sort of tests I have written for my view controllers using GTM, Hamcrest, and OCMock (our iPhone Unit Testing Toolkit). Hopefully this can serve as a starting point for the tests you could be writing for your own projects.

Testing Interface Builder Bindings
Broken nib bindings appear to be a common cause of application bugs during development. It is certainly easy enough to accidentally break or forget to create a binding while editing a nib file so let’s write some simple tests to assert that our actions and outlets are actually connected to objects in a nib file.
These are really tests of the nib file itself. If the goal was to test the view controller’s use of these bound view objects I would replace the views with mock objects which could verify the controller’s behavior. (Erik Dörnenburg has provided a nice example of doing just that: Testing Cocoa Controllers with OCMock)

- (void) testViewBinding {
	TestableSimpleViewController *viewController = [[TestableSimpleViewController alloc] initWithNibName:@"TestableSimpleViewController" bundle:nil];
	[viewController loadView]; //It is not strictly necessary to call loadView for this test as we access the view property which will call loadView if view is nil
	assertThat(viewController.view, isNot(nilValue()));
	assertThat(viewController.button, isNot(nilValue()));
}

- (void) testUIButtonActionBinding {
	TestableSimpleViewController *viewController = [[TestableSimpleViewController alloc] initWithNibName:@"TestableSimpleViewController" bundle:nil];
	[viewController loadView]; //Here we must call loadView since we have not accessed the controller's view property to trigger view loading from the nib
	assertThat([viewController.button actionsForTarget:viewController forControlEvent:UIControlEventTouchUpInside], onlyContains(@"testAction", nil));
}

Testing View Reloading
View controllers should be able to unload their views if the application receives a memory warning while the view is not visible and then reload the view when it is needed again. We can reproduce that same sequence of messages in a test. For a non-trivial controller we would add additional assertions to test that any other view dependent properties were correctly released and recreated as the view was unloaded and reloaded.

- (void) testViewUnloading {
	TestableSimpleViewController *viewController = [[TestableSimpleViewController alloc] initWithNibName:@"TestableSimpleViewController" bundle:nil];
	[viewController loadView];
	assertThat(viewController.view, isNot(nilValue()));
	[viewController didReceiveMemoryWarning];
	assertThat(viewController.button, is(nilValue())); //Note that while viewController.view is nil here we cannot test that directly as accessing view will trigger a call to loadView. Instead we can only test that our outlets have been released as expected.
	[viewController loadView];
	assertThat(viewController.view, isNot(nilValue()));
}

Testing Switching Between View Controllers
View controllers frequently have dependencies on other view controllers. If those dependencies are tightly coupled it becomes very difficult to test the behavior of a single view controller. Instead we can expose those dependencies and replace them with mock objects in order to isolate the controller we are currently testing. Here we have a simple table view controller which presents a detail view controller when a table cell is selected. By mocking both the navigation controller used to present that detail view and the detail view itself we can capture and verify the table view controller’s behavior.

- (void) testSelectingACellPushesADetailView {
	id detailViewController = [OCMockObject niceMockForClass:[GenericDetailViewController class]];
	NSDictionary *data = [[NSDictionary alloc] init];
	UINavigationController *navController = [[UINavigationController alloc] init];
	id navigationController = [OCMockObject partialMockForObject:navController];

	TestableTableViewController *viewController = [[TestableTableViewController alloc] initWithNibName:@"TestableTableViewController" bundle:nil];
	viewController.detailViewController = detailViewController;
	viewController.modelData = data;

	[navigationController pushViewController:viewController animated:NO];
	assertThat(viewController.navigationController, isNot(nilValue()));

	[viewController loadView];
	[[detailViewController expect] setModelData:data]; //expect that the detail view will be given some data to display
	[[navigationController expect] pushViewController:detailViewController animated:YES]; //expect that the detail view will be pushed onto the nave controller
	[viewController tableView:viewController.tableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
	[detailViewController verify];
	[navigationController verify];
}

So what makes test-driven controllers different?
I certainly could have written these example controllers without any tests around them. However by writing tests I was encouraged to make a couple of changes to the way I structure my code.

  • My controllers expose dependencies that might otherwise remain buried in their implementations. In the example above I exposed the detail view controller my controller used so that I could replace it with a mock object. That made it possible to test that the expected data was passed to the detail view but it will also make it easy to switch to a different detail view controller in the future.
  • By focusing on testing just one class at a time I am encouraged to create well defined interfaces and a clear set of responsibilities for each class. That in turn leads to more modular code which is easier to maintain and reuse.
  • I write bugs but if I write a test to reproduce them and then prove that they are fixed I shouldn’t ever see the same bug twice. It is much easier to catch a missing nib binding because a test failed than it is to see the app crash when displaying some view and have to track down that exc_bad_access error in the debugger.
  • Most importantly by writing tests I can have confidence that my code works as I expected, not only when I first write it but also every time I run my tests (which happens every time I build the app). With that feedback I can change one controller without fearing that I might have broken something somewhere else in the app.

Feedback

  Comments: 10


  1. Thanks a lot for this article, especially for the uinavigationcontroller testing technique.


  2. Thanks for the instructions. I am attempting to set up these tests as a Logic Test (currently I am using assertions in the viewWillAppear controller method). I am passing the nib name (I’ve checked it, it is correct) and am passing nil for the bundle. However, I get the error:

    /Users/eeyore/Developer/iSplit/Test/Logic/RacerViews/MWAddRacerControllerTests.m:19:0 (theController.view) != nil fails raised -[UIViewController _loadViewFromNibNamed:bundle:] was unable to load a nib named “MWAddRacerController”.

    I suspect the Logic Test is looking in the wrong bundle for the Nib file and that these tests need to be Application Tests. Is that correct? If it isn’t, I can post code.

    I tried searching for this question and got one hit on Stack Overflow, but no answers were being accepted there.

    Thanks,
    Aaron


  3. Thanks for this, very helpful. One thing I did notice however when I tried it is that when I call [viewController loadView] in a test, seemingly nothing happens. Maybe this is triggering that the view should load, but because tests are running on the main thread they never get around to it? All I know is that I have to actually poke at the view for it to load. (This was tested by setting a breakpoint on viewDidLoad, it only fired if I tried to look at viewController.view, not if I just called loadView.)

    Know what’s going on here?


    • Calling -loadView directly probably isn’t a great idea on my part. -loadView should really only be called by UIViewController's internals so the behavior of what exactly triggers callbacks like -viewDidLoad could certainly have changed with iOS releases since I first wrote this.

      A better solution is probably to access the view property directly. I initially avoided that because the side effects of calling -view may not be obvious to someone reading the test but STAssertNotNil(controller.view) might be a better way to express that we want to make sure the view is loaded.


  4. Useful – thanks 🙂


  5. Can’t you use -[UIViewController isViewLoaded] to determine whether the view was unloaded after receiving a memory warning?


    • Sure ‘isViewLoaded’ will tell you if the view was unloaded but that’s behavior provided by UIViewController which isn’t normally what I want to test. I want to make sure that my UIViewController subclass’ -viewDidUnload method is releasing references to view objects. Testing my subclass, not Apple’s frameworks.


  6. Nice!
    Thanks a lot!


  7. Thanks for the post. I am curious how you’re testing IBOutlets now that the recommended approach is to declare IBOutlet properties in a class extension in the implementation file.


    • That’s not an approach I usually follow. I try to write controllers with well defined public dependencies on their views (which may be set via outlet bindings). I find that private view properties encourage coupling the controller to its view and lead toward a monolithic controller-blob which spans both the view and controller layers. A public interface to the view, possibly exposed as a protocol, also helps me keep track of what my contract is with the nib and I think helps allow other developers or designers to work on the view without worrying about breaking behavior tied to the controller. If you’re doing lots of view work, like localized nibs, I’d write view tests to verify that the views conform to that interface as well.

Your feedback