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.