In my last post on unit testing iPhone development I introduced a couple helpful tools including OCMock, a mock objects implementation for Objective-C.
I recently came across a scenario where I needed to make an assertion on an argument passed to an expected method invocation but OCMock did not provide the constraint I needed. I was testing a FileDetailsViewController that pushes a new FileContentsViewController on to the view stack when a button is pressed. I mocked the UINavigationController so that I could add an assertion that pushViewController:animated: is called with the right arguments.
The meat of my test is:
@interface FileDetailsViewControllerTest : SenTestCase {} FileDetailsViewController *controller; id navigationController; @end @implementation FileDetailsViewControllerTest -(void) setUp { controller = [[FileDetailsViewController alloc] initWithNibName:@"FileDetailsView" bundle:nil]; navigationController = [OCMockObject mockForClass:[UINavigationController class]]; controller.navigationController = navigationController; } -(void) testShowContentLoadsView { [[navigationController expect] pushViewController:[OCMConstraint isKindOfClass:[FileContentsViewController class]] animated:YES]; [controller showContentsButtonWasPressed]; } -(void) tearDown { [navigationController verify]; } @end
Note that in this example, I some fanciness is required to assign controller.navigationController = navigationController since it is a readonly property in the UIViewController API. I’ll explain what I did in a later post.
The OCMock feature I was missing is the OCMock.isKindOfClass constraint. So I wrote my own.
Following is the header file that declares the isKindOfClass assertion as a static method in an Objective-C category for OCMock. Categories are kind of like Ruby mixins for Objective-C. You can add static or instance methods to an existing class definition. With this approach I can follow the OCMock pattern of providing static factory methods on the OCMock class for the range of constraints available.
//File: OCMockConstraint+Extensions.h #import <OCMock/OCMConstraint.h> @interface OCMConstraint (Extensions) + (id)isKindOfClass:(id)value; @end
Following is the implementation, both of my new constraint and the factory method to create it.
//File: OCMockConstraint+Extensions.m #import "OCMockConstraint+Extensions.h" @interface OCMKindOfClassConstraint : OCMConstraint { @public id testClass; } @end @implementation OCMKindOfClassConstraint - (BOOL)evaluate:(id)value { return [value isKindOfClass:testClass]; } @end // Static factory method @implementation OCMConstraint (Extensions) + (id)isKindOfClass:(id)value { OCMKindOfClassConstraint *constraint = [OCMKindOfClassConstraint constraint]; constraint->testClass = value; return constraint; } @end
This implementation shows both how to write a custom constraint for OCMock and a use of Objective-C categories to provide a nice API for using it.
In many cases, you’ll want to write a custom constraint that is specific to your test scenario and not appropriate to be shared globally in this manner. In this case you’ll probably follow a similar strategy but implemented in your test class instead of mixed in to OCMockConstraint.


I’m curious about the fancy navigation controller assignment you referred to in your post.
Keith,
You need to some dependency injection with the controller (assume it’s called RootController). Keep you nib file basic and create the controller programatically. In the interface add (with usual @synthesize and dealloc stuff):
@interface RootViewController : UITableViewController
{
UINavigationController *navigationController;
}
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;
And in the creation (e.g. the AppDelegate) add an extra line to manually set the navigation controller:
RootViewController* rootController =
[[RootViewController alloc] initWithNibName:@”RootViewController” bundle:nil];
navigationController =
[[UINavigationController alloc] initWithRootViewController:rootController];
rootController.navigationController = navigationController;
Then you can do the controller testing and mocking described so well above.
How can i run OCMock on device?