Objective-c – How to stub a class method in OCMock

cocoa-touchiphoneobjective cocmock

I often find in my iPhone Objective-C unit tests that I want stub out a class method, e.g. NSUrlConnection's +sendSynchronousRequest:returningResponse:error: method.

Simplified example:

- (void)testClassMock
{
    id mock = [OCMockObject mockForClass:[NSURLConnection class]];
    [[[mock stub] andReturn:nil] sendSynchronousRequest:nil returningResponse:nil error:nil];
}

When running this, I get:

Test Case '-[WorklistTest testClassMock]' started.
Unknown.m:0: error: -[WorklistTest testClassMock] : *** -[NSProxy doesNotRecognizeSelector:sendSynchronousRequest:returningResponse:error:] called!
Test Case '-[WorklistTest testClassMock]' failed (0.000 seconds).

I've had a really hard time finding any documentation on this, but I assume that class methods aren't supported by OCMock.

I found this tip after a lot of Googling. It works, but is very cumbersome:
http://thom.org.uk/2009/05/09/mocking-class-methods-in-objective-c/

Is there anyway to do this within OCMock? Or can someone think of a clever OCMock category object that could be written to accomplish this sort of thing?

Best Answer

Update for OCMock 3

OCMock has modernized its syntax for supporting class method stubbing:

id classMock = OCMClassMock([SomeClass class]);
OCMStub(ClassMethod([classMock aMethod])).andReturn(aValue);

Update

OCMock now supports class method stubbing out of the box. The OP's code should now work as posted. If there is an instance method with the same name as the class method, the syntax is:

[[[[mock stub] classMethod] andReturn:aValue] aMethod]

See OCMock's Features.

Original Answer

Sample code following Barry Wark's answer.

The fake class, just stubbing connectionWithRequest:delegate:

@interface FakeNSURLConnection : NSURLConnection
+ (id)sharedInstance;
+ (void)setSharedInstance:(id)sharedInstance;
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
@end
@implementation FakeNSURLConnection
static id _sharedInstance;
+ (id)sharedInstance { if (!_sharedInstance) { _sharedInstance = [self init]; } return _sharedInstance; }
+ (void)setSharedInstance:(id)sharedInstance { _sharedInstance = sharedInstance; }
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate {
    return [FakeNSURLConnection.sharedInstance connectionWithRequest:request delegate:delegate];
}
- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate { return nil; }
@end

Switching to and from the mock:

{
    ...
    // Create the mock and swap it in
    id nsurlConnectionMock = [OCMockObject niceMockForClass:FakeNSURLConnection.class];
    [FakeNSURLConnection setSharedInstance:nsurlConnectionMock];
    Method urlOriginalMethod = class_getClassMethod(NSURLConnection.class, @selector(connectionWithRequest:delegate:));
    Method urlNewMethod = class_getClassMethod(FakeNSURLConnection.class, @selector(connectionWithRequest:delegate:));
    method_exchangeImplementations(urlOriginalMethod, urlNewMethod);

    [[nsurlConnectionMock expect] connectionWithRequest:OCMOCK_ANY delegate:OCMOCK_ANY];

    ...
    // Make the call which will do the connectionWithRequest:delegate call
    ...

    // Verify
    [nsurlConnectionMock verify];

    // Unmock
    method_exchangeImplementations(urlNewMethod, urlOriginalMethod);
}