How to stub a class method in OCMock?
Asked Answered
L

5

41

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?

Lissettelissi answered 27/11, 2009 at 18:12 Comment(3)
New URL of blog post: thomlawrence.wordpress.com/2009/05/09/…Eudoca
Original blog post link is good, the wordpress.com link is dead.Stall
Starting with OCMock release 2.1 this is supported out of the box. You can now stub class methods in the same way as you stub instance methods.Cassatt
R
19

Coming from the world of Ruby, I understand exactly what you're trying to accomplish. Apparently, you were literally three hours ahead of me trying to do exactly the same thing today (time zone thing? :-).

Anyway, I believe that this is not supported in the way one would desire in OCMock because stubbing a class method needs to literally reach into the class and changes its method implementation regardless of when, where, or who calls the method. This is in contrast to what OCMock seems to do which is to provide you a proxy object that you manipulate and otherwise operate on directly and in lieu of a "real" object of the specified class.

For example, it seems reasonable to want to stub NSURLConnection +sendSynchronousRequest:returningResponse:error: method. However, it is typical that the use of this call within our code is somewhat buried, thus making it very awkward to parameterize it and swap in a mock object for the NSURLConnection class.

For this reason, I think the "method swizzling" approach you've discovered, while not sexy, is exactly what you want to do for stubbing class methods. To say it's very cumbersome seems extreme -- how about we agree it's "inelegant" and maybe not as convenient as OCMock makes life for us. Nevertheless, it's a pretty concise solution to the problem.

Regenerative answered 28/11, 2009 at 0:0 Comment(2)
Fair enough, I'll accept "inelegant" or "cumbersome" without the very. ;). Even though I know it reaches into the class and changes the method implementation, I still wish I could do that via OCMock as long as I can undo it at the end of the test. On a related note, is it possible to make that sort of pervasive change on instances methods using OCMock? i.e. change all objects of type NSString to have a different implementation of -lowercaseString? I know you can do this with method swizzling but I would occasionally find it convenient when using OCMock as well.Lissettelissi
If you're coming here via a search, this is now supported out of the box in OCMock. You can now stub class methods in the same way as you stub instance methods.Gildagildas
P
48

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);
}
Paw answered 5/12, 2012 at 0:11 Comment(2)
thanks for the update on OCMock supporting class stubing out of the box! I want to check a class method is called with certain parameters (using expect). Do you know if this is possible too?Illusage
@Illusage Definitely. Expect, reject, stub -- all function as they would with instance methods.Paw
R
19

Coming from the world of Ruby, I understand exactly what you're trying to accomplish. Apparently, you were literally three hours ahead of me trying to do exactly the same thing today (time zone thing? :-).

Anyway, I believe that this is not supported in the way one would desire in OCMock because stubbing a class method needs to literally reach into the class and changes its method implementation regardless of when, where, or who calls the method. This is in contrast to what OCMock seems to do which is to provide you a proxy object that you manipulate and otherwise operate on directly and in lieu of a "real" object of the specified class.

For example, it seems reasonable to want to stub NSURLConnection +sendSynchronousRequest:returningResponse:error: method. However, it is typical that the use of this call within our code is somewhat buried, thus making it very awkward to parameterize it and swap in a mock object for the NSURLConnection class.

For this reason, I think the "method swizzling" approach you've discovered, while not sexy, is exactly what you want to do for stubbing class methods. To say it's very cumbersome seems extreme -- how about we agree it's "inelegant" and maybe not as convenient as OCMock makes life for us. Nevertheless, it's a pretty concise solution to the problem.

Regenerative answered 28/11, 2009 at 0:0 Comment(2)
Fair enough, I'll accept "inelegant" or "cumbersome" without the very. ;). Even though I know it reaches into the class and changes the method implementation, I still wish I could do that via OCMock as long as I can undo it at the end of the test. On a related note, is it possible to make that sort of pervasive change on instances methods using OCMock? i.e. change all objects of type NSString to have a different implementation of -lowercaseString? I know you can do this with method swizzling but I would occasionally find it convenient when using OCMock as well.Lissettelissi
If you're coming here via a search, this is now supported out of the box in OCMock. You can now stub class methods in the same way as you stub instance methods.Gildagildas
E
6

Here is a nice 'gist' with a swizzle implementation for class methods: https://gist.github.com/314009

Eudoca answered 21/2, 2011 at 2:27 Comment(2)
I stumbled across an improved class method 'swizzler': gist.github.com/1038034Ose
Note reborgs comment on the gist and change Method mockMethod = class_getInstanceMethod(aNewClass, aNewMethod); into Method mockMethod = class_getClassMethod(aNewClass, aNewMethod);Catalogue
B
4

If you modify your method under test to take a parameter which injects the class of the NSURLConnection, then it's relatively easy to pass in a mock that responds to the given selector (you may have to create a dummy class in your test module which has the selector as an instance method and mock that class). Without this injection, you're using a class method, essentially using NSURLConnection (the class) as a singleton and hence have fallen into the anti-pattern of using singleton objects and the testability of your code has suffered.

Burrill answered 28/11, 2009 at 0:49 Comment(0)
T
2

Link to the blogpost in the question and RefuX gist inspired me to come up with block enabled implementation of their ideas: https://gist.github.com/1038034

Tupper answered 21/6, 2011 at 15:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.