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.

8 Responses to Custom Constraints for OCMock

  1. Keith says:

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

  2. John says:

    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.

  3. Artem says:

    How can i run OCMock on device?

  4. Christopher Pickslay says:

    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;

  5. Christopher Pickslay says:

    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.

  6. Jon Reid says:

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

  7. Yujianhua1982 says:

    Very helpful.

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

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

  8. Pingback: Mocking UITableViewCell and using isKindOfClass | BlogoSfera

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>