Testing class method using OCMock release 2.1.1
Asked Answered
A

2

0

I am trying to check if a class method is getting invoked using OCMock. I have gathered from OCMock website and other answers on SO that the new OCMock release (2.1) adds support for stubbing class methods.

I am trying to do the same:

DetailViewController:

+(BOOL)getBoolVal
{
return YES;
}

Test Case:

-(void) testClassMethod
{
id detailMock = [OCMockObject mockForClass:[DetailViewController class]];

[[[detailMock stub] andReturnValue:OCMOCK_VALUE((BOOL){YES})] getBoolVal:nil];
}

The test is running and also succeeding but it succeeds even if I return NO instead of YES from getBoolVal method in DetailViewController. On keeping a breakpoint on that method, the test execution does not stop indicating that the method is not called.

How do I check a class method then?

Amathist answered 9/5, 2013 at 11:59 Comment(0)
C
0

EDIT

Just looked more closely at the latest version of OCMock and I think you're correct in interpreting the way to mock a class method that you're directly testing, so is the issue simply that you're calling it with the wrong signature?

The answer below is a general case (so also useful when a method under test calls out to a class method).

Original

Checking for a class method with OCMock is a little tricky. What you are currently doing is creating a mock object called detailMock and stubbing an instance method called getBoolVal: (By the way, your method prototype doesn't take an argument, so you shouldn't be passing nil to it -- to get really nit-picky, if you want to follow Apple's guidelines, they recommend not using the word "get" in a getter (unless you're sending a pointer reference to get set)). Compilation doesn't fail because detailMock is an id and willing to respond to any selector.

So how to test a Class method? For the general case, you'll need to do some swizzling. Here is how I do it.

Let's look at how we'd fake NSURLConnection which you should be able to apply to your class as well.

Start by extending your class:

@interface FakeNSURLConnection : NSURLConnection

+ (id)sharedInstance;
+ (void)setSharedInstance:(id)sharedInstance;
+ (void)enableMock:(id)mock;
+ (void)disableMock;

- (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate;
@end

Note that I'm interested in testing connectionWithRequest:delegate and that I've extended the class to add a public instance method with the same signature as the class method. Let's look at the implementation:

@implementation FakeNSURLConnection

SHARED_INSTANCE_IMPL(FakeNSURLConnection);    
SWAP_METHODS_IMPL(NSURLConnection, FakeNSURLConnection);    
DISABLE_MOCK_IMPL(FakeNSURLConnection);    
ENABLE_MOCK_IMPL(FakeNSURLConnection);    

+ (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

So what's going on here? First there are some macros which I will discuss below. Next I have overridden the class method to have it call the instance method. We can use OCMock to mock instance methods, so by having the class method call the instance method, we can have the class method call the mock.

We don't want to use FakeNSURLConnection in our real code though, but we do want to use it in our testing. How can we do this? We can swizzle the class methods between NSURLConnection and FakeNSURLConnection. That means that after we swizzle a call to NSURLConnection connectionWithRequest:delegate with call FakeNSURLConnection connectionWithRequest:delegate. That brings us to our macros:

#define SWAP_METHODS_IMPL(REAL, FAKE) \
+ (void)swapMethods \
{ \
    Method original, mock; \
    unsigned int count; \
    Method *methodList = class_copyMethodList(object_getClass(REAL.class), &count); \
    for (int i = 0; i < count; i++) \
    { \
        original = class_getClassMethod(REAL.class, method_getName(methodList[i])); \
        mock = class_getClassMethod(FAKE.class, method_getName(methodList[i])); \
        method_exchangeImplementations(original, mock); \
    } \
    free(methodList); \
}

#define DISABLE_MOCK_IMPL(FAKE) \
+ (void)disableMock \
{ \
    if (_mockEnabled) \
    { \
        [FAKE swapMethods]; \
        _mockEnabled = NO; \
    } \
}

#define ENABLE_MOCK_IMPL(FAKE) \
static BOOL _mockEnabled = NO; \
+ (void)enableMock:(id)mockObject; \
{ \
    if (!_mockEnabled) \
    { \
        [FAKE setSharedInstance:mockObject]; \
        [FAKE swapMethods]; \
        _mockEnabled = YES; \
    } \
    else \
    { \
        [FAKE disableMock]; \
        [FAKE enableMock:mockObject]; \
    } \
}

#define SHARED_INSTANCE_IMPL() \
+ (id)sharedInstance \
{ \
    return _sharedInstance; \
}

#define SET_SHARED_INSTANCE_IMPL() \
+ (void)setSharedInstance:(id)sharedInstance \
{ \
    _sharedInstance = sharedInstance; \
}

I'd recommend something like this so you don't accidentally re-swizzle your class methods. So how would you use this?

id urlConnectionMock = [OCMockObject niceMockForClass:FakeNSURLConnection.class];
[FakeNSURLConnection enableMock:urlConnectionMock];
[_mocksToDisable addObject:FakeNSURLConnection.class];

[[[urlConnectionMock expect] andReturn:urlConnectionMock] connectionWithRequest:OCMOCK_ANY delegate:OCMOCK_ANY];

That's pretty much it -- you've swizzled the methods so your fake class will get called and that will call your mock.

Ah, but one last thing. _mocksToDisable is an NSMutableArray that will contain a class object for every class we've swizzled.

- (void)tearDown
{
    for (id mockToDisable in _mocksToDisable)
    {
        [mockToDisable disableMock];
    }
}

We do this in tearDown to make sure we have unswizzled our class after the test has run -- don't do it right in the test because if there is an exception not all your test code may get executed but tearDown always will be.

There may be other mock technologies that make this simpler, though I've found it's not so bad since you write it once and can use it many times.

Choker answered 10/5, 2013 at 16:42 Comment(0)
B
0

Maybe I'm missing something here (and I know this is a bit old), but your class signature is +(BOOL)getBoolVal { return YES; } and in your test, you're calling expect on getBoolVal:nil

Those don't match, right? In that case, your class mock will say, "Oh, that's not the signature I expect" and try to pass it on to the underyling class, I believe. See the forwardInvocationForClassObjectin the OCMock source.

As far as why you're getting NO (since the underlying class also returns YES, which makes this test kind of moot, but that's another issue), I'm not 100% sure, but maybe it's just a C-ism -- 'indeterminate value, void, 0 (false/NO)"

Bickerstaff answered 15/8, 2013 at 16:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.