Testing Cocoa Controllers with OCMock

4 July 2008

For a few releases the Apple development tools have included OCUnit and many developers have now started to write unit tests. There are lots of tutorials that explain how this is done for the straight-forward cases but there’s one area of testing that has proven difficult on most platforms, and that is testing of the user interface. That said, there are a few things that make this an easier problem to solve with Cocoa and in this post I’ll explain why.

Let’s look at an example. CCMenu is a small application that displays the status of CruiseControl continuous integration servers. As part of adding a new project to be monitored the user has to enter the URL for the CruiseControl server and a combox provides a history of previously used servers.

The same dialog has a matrix of buttons to set the type of the server. Actually, by default CCMenu detects the server type automatically but that’s beside the point. What we’re interested in is the piece of functionality that is responsible for selecting the correct server type for the URL chosen by the user. In our case, this would be Cruise Control Dashboard.

One difference between Cocoa and most other UI frameworks is the fact that the user interface is stored by serialising the objects, rather than generating code. For this reason I’m happy to not insist on test-first for the actual interface. What I do want to test is the code in the controller classes.

Obviously, I could load the serialised objects, locate the elements involved and then use methods such as performClick: to trigger the actions. Sounds convoluted? Yes, I agree. Luckily there’s a better way.

This is a case where testing is tricky because the object under test interacts with objects that are difficult to deal with. In such cases dynamic mock objects have proven extremely useful. A good introduction can be found here and mockobject.com lists papers and implementations. For Objective-C I created OCMock. Let’s look at how this helps us testing our controllers.

Here are the relevant parts of the interface declaration of the controller. Given that I’m doing test-driven development there’s currently no implementation of the methods. I simply created the interface of the controller based on my needs while designing the dialog.

@interface CCMPreferencesController : CCMWindowController
{
    IBOutlet NSComboBox *serverUrlComboBox;
    IBOutlet NSMatrix *serverTypeMatrix;
}
 
- (IBAction)historyURLSelected:(id)sender;

In my test I want to use mock objects instead of loading the actual user interface. So, in the test setup, after creating my controller, I create appropriate mock objects and set up the controller to use these.

@implementation CCMPreferencesControllerTest
 
- (void)setUp
{
    controller = [[[CCMPreferencesController alloc] init] autorelease];
    serverUrlComboBoxMock = [OCMockObject mockForClass:[NSComboBox class]];
    [controller setValue:serverUrlComboBoxMock forKey:@"serverUrlComboBox"];
    serverTypeMatrixMock = [OCMockObject mockForClass:[NSMatrix class]];
    [controller setValue:serverTypeMatrixMock forKey:@"serverTypeMatrix"];
}

You notice that I’m using key-value coding to set the variables (outlets). I’m doing this because the variables are not public and at the same time I don’t want to write accessor methods for them.

Now, we can start writing the actual test for the functionality. Have a look at the full test first.

- (void)testSelectsServerTypeWhenHistoryURLIsSelected
{
    NSString *selectedUrl = @"http://localhost/cctray.xml";
    [[[serverUrlComboBoxMock stub] andReturn:selectedUrl] stringValue];
    [[serverTypeMatrixMock expect] selectCellWithTag:CCMCruiseControlDashboard];
    [controller historyURLSelected:nil];
}

What’s going on here? Firstly, I tell the mock object that stands in for the actual combox box that it should stub the stringValue method. This means that when somebody invokes stringValue on this object it will return the string @”http://localhost/cctray.xml”. This is all we’d have done with the real combo box anyway.

Secondly, I tell the mock that stands in for the server type matrix that it should expect that the method selectCellWithTag: is called with CCMCruiseControlDashboard as an argument.

Lastly, I invoke the method I want to test. What happens, once the implementation is complete, is that the code will go the the combo box and ask it for its string value. The first mock will return the stubbed value. Now, the code should do whatever it needs to do to figure out which server type this corresponds to and then set that in the server type matrix by selecting the cell with the appropriate tag, and this is what we’ve told the second mock to expect.

Wait, you might say, we don’t have an implementation yet. So, how does this test fail? It doesn’t yet. We’ll have to tell the mock objects to verify that everything we told them to expect actually occurred, and a logical place for this is the test tearDown.

- (void)tearDown
{
    [serverUrlComboBoxMock verify];
    [serverTypeMatrixMock verify];
}

Strictly speaking, we don’t have to verify the combo box mock because it doesn’t have any expectations but it’s good practice to verify all mocks in the tear down, especially if the same mocks are used for multiple tests. By the way, by default the mocks also have fail-fast behaviour; when they receive a method that wasn’t stubbed or expected, they raise an exception right away. Detecting something that wasn’t expected can be done right way, detecting that something that was expected didn’t occur must be trigger by the user.

Now, if we run this test with an empty implementation of historyURLSelected: it will fail when we tell the server type matrix mock to verify because the expected method hasn’t been called. The error message will look something like this:

Unknown.m:0: error: -[CCMPreferencesControllerTest testSelectsServerTypeWhenHistoryURLIsSelected] : OCMockObject[NSMatrix]: expected method was not invoked: selectCellWithTag:0

Adding an implementation like the following one adds the right functionality and makes our test pass.

@implementation CCMPreferencesController
 
- (void)historyURLSelected:(id)sender
{
    NSString *serverUrl = [serverUrlComboBox stringValue];
    [serverTypeMatrix selectCellWithTag:[serverUrl cruiseControlServerType]];
}

In summary, testing controllers becomes relatively easy when we follow this pattern:

1. Replace all UI elements with mocks; using key-value coding to access the outlets.

2. Set up stubs with return values for UI elements that the controller will query.

3. Set up expectations for UI elements that the controller should manipulate.

4. Invoke the method in the controller.

5. Verify the expectations.

I find tests following this pattern easier to write and understand than tests that load a NIB file and interact with the actual user interface elements.

5 comments

  1. Testing View Controllers « Carbon Five Community

    11 March 2010, 05:01

    […] 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 = […]

  2. References on Unit Testing & UI Automation for iOS Applications | Jojit Soriano's Blog

    3 June 2011, 12:03

    […] View Controllers Remarks: Sample Unit Testing of a view controller. Sample use of OCMock also. – Testing Cocoa Controllers with OCMock by Erik Dornenburg – TDD Best Practices: Unit Testing in iOS with GHUnit (Part 1) – Unit Testing […]

  3. Testing iPhone View Controllers | The Carbon Emitter

    11 June 2011, 10:53

    […] 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) […]

  4. Mobile Testing

    25 October 2011, 00:09

    Thank you for providing this detailed explanation regarding testing of the user interface and for the quick summary at the end. This post was very helpful to me, as I’m sure it was for a number of other people struggling with UI testing. Using dynamic mock objects definitely makes testing easier!

  5. Using OCMock with Mac OS X Lion, Xcode 4, to Mock and Unit Test Cocoa Desktop Apps « Procbits

    30 November 2011, 04:07

    […] Testing Cocoa Controllers with OCMock […]

Leave a comment