Custom Constraints for OCMock

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 

@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.

This entry was posted in Mobile, Process and tagged , , . Bookmark the permalink.
  • http://www.groovysquared.com Keith

    I’m curious about the fancy navigation controller assignment you referred to in your post.

  • John

    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.

  • Artem

    How can i run OCMock on device?

  • Christopher Pickslay

    Alon-

    Thanks for this–it was just what I was looking for. One thing, though–when I tried using your sample code in XCode 3.2.1, the test harness crashed on the expect call. Same thing when I tried implementing it for isMemberOfClass. However, if I change the method name to “isA”, it works fine:

    @interface OCMConstraint (Extensions)

    + (id)isA:(id)value;

    @end

    Perhaps there’s some name collision because NSObject defines isKindOfClass as an instance method with different argument and return types:

    – (BOOL)isKindOfClass:(Class)aClass;

  • Christopher Pickslay

    Update: one day later, I decided to refactor my code to pass a protocol instance to the method in the class I’m mocking. So I copied my working constraint and reimplemented it to test for conformsToProtocol: instead of isKindOfClass:

    @interface OCMConformsToProtocolConstraint : OCMConstraint {

    @public
    id testProtocol;
    }

    @end

    @implementation OCMConformsToProtocolConstraint

    – (BOOL)evaluate:(id)value {
    return [value conformsToProtocol:testProtocol];
    }

    @end

    @implementation OCMConstraint (Extensions)

    + (id)conformsTo:(id)value {
    OCMConformsToProtocolConstraint *constraint = [OCMConformsToProtocolConstraint constraint];
    constraint->testProtocol = value;
    return constraint;
    }

    @end

    Unfortunately, OCMockObject doesn’t recognize that the method invocation matches this constraint, so it never gets evaluated, and I get “unexpected method invoked”.

    So I went looking for a Hamcrest matcher that would work. isCompatibleType (from http://code.google.com/p/hamcrest/wiki/TutorialObjectiveC) looked promising, but doesn’t appear to exist in OCHamcrest, so that was out.

    I finally got it working by using OCMArg checkWithSelector:aSelector onObject:anObject, which allows you to define an arbitrary method to evaluate whether the call matches. It’s ugly, but it works for now:

    -(BOOL) conformsToSomeInterface:(id)arg {
    return [arg conformsToProtocol:@protocol(someInterface)];
    }

    -(void) testSomething {
    id mockDependency = [OCMockObject mockForClass:[Dependency class]];
    [[mockDependency expect] someMethod:[OCMArg checkWithSelector:@selector(conformsToSomeInterface:) onObject:self]];
    [controller setDependency:mockDependency];

    [mockDependency verify];
    }

    Next step: implement a custom hamcrest matcher to do this.

  • Jon Reid

    Christopher, note that one of the biggest features of Hamcrest is its extensibility. It should be fairly trivial to write a “conformsToProtocol” matcher. (…But I may just go ahead and add it to OCHamcrest.)

  • Yujianhua1982

    Very helpful.

    Plus one question:
    navigationController = [OCMockObject mockForClass:[UINavigationController class]];
    controller.navigationController = navigationController;

    navigationController is read only property, we can’t set it.

  • Pingback: Mocking UITableViewCell and using isKindOfClass | BlogoSfera