Testing iPhone View Controllers

Jonah Williams ·

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.