How to mock class method (+)? [duplicate]
Asked Answered
L

2

5

Need to write unit testing for the following code, I want to do mock for class method canMakePayments, return yes or no, so far no good method found dues to canMakePayments is a class method (+), seems all OCMock methods are all used for instance method (-).

You guys any suggestion or discussion will be appreciated. Thanks.

// SKPaymentQueue.h
// StoreKit
if ([SKPaymentQueue canMakePayments]){
   ....
}
else{
   ...
}
Linton answered 8/12, 2011 at 6:24 Comment(1)
Point at it and laugh? (sorry, I couldn't resist)Palmate
B
6

Since you can't intercept the method by providing a different instance, what you can do for a class method is provide a different class. Something like this:

+ (Class)paymentQueueClass
{
    return [SKPaymentQueue class];
}

The point of call then becomes:

Class paymentQueueClass = [[self class] paymentQueueClass];
if ([paymentQueueClass canMakePayments])
...

This introduces a "testing seam," or a point of control, allowing us to specify a class other than SKPaymentQueue. Now let's make a replacement:

static BOOL fakeCanMakePayments;

@interface FakePaymentQueue : SKPaymentQueue
@end

@implementation FakePaymentQueue

+ (void)setFakeCanMakePayments:(BOOL)fakeValue
{
    fakeCanMakePayments = fakeValue;
}

+ (BOOL)canMakePayments
{
    return fakeCanMakePayments;
}

@end

Strictly speaking, this isn't a "mock object" -- it's a "fake object." The difference is that a mock object verifies how it's called. A fake object just provides stubbed results.

Now let's create a testing subclass of the original class we want to test.

@interface TestingSubclass : OriginalClass
@end

@implementation TestingSubclass

+ (Class)paymentQueueClass
{
    return [FakePaymentQueue class];
}

@end

So you see, this replaces SKPaymentQueue with FakePaymentQueue. Your tests can now run against TestingSubclass.

Balzac answered 8/12, 2011 at 6:41 Comment(4)
What is the class under test? That is, are you writing a test against SKPaymentQueue, or against a different class that calls SKPaymentQueue?Balzac
Hey Reid; thanks for your kindly reply. Do you mean we need to implement another paymentQueueClass in our testing class? Or hack original framework method category (UnitTests)? Seems I don't fully understand your solution, could you give me more tips, thanks very much.Linton
My testing class is against a different class that calls SKPaymentQueue?Linton
Yes, you are right, we can use fake method for this kind of tough issue.Linton
D
14

One approach is to wrap the class method in your own instance method:

-(BOOL)canMakePayments {
    return [SKPaymentQueue canMakePayments];
}

Then you mock that method:

-(void)testCanHandlePaymentsDisabled {
    Foo *foo = [[Foo alloc] init];
    id mockFoo = [OCMockObject partialMockForObject:foo];
    BOOL paymentsEnabled = NO;
    [[[mockFoo stub] andReturnValue:OCMOCK_VALUE(paymentsEnabled)] canMakePayments];

    // set up expectations for payments disabled case
    ...

    [foo attemptPurchase];
}
Demetrademetre answered 8/12, 2011 at 16:48 Comment(1)
Thanks your reply, yes, code should be written in a way which is easier to do unit testing.Linton
B
6

Since you can't intercept the method by providing a different instance, what you can do for a class method is provide a different class. Something like this:

+ (Class)paymentQueueClass
{
    return [SKPaymentQueue class];
}

The point of call then becomes:

Class paymentQueueClass = [[self class] paymentQueueClass];
if ([paymentQueueClass canMakePayments])
...

This introduces a "testing seam," or a point of control, allowing us to specify a class other than SKPaymentQueue. Now let's make a replacement:

static BOOL fakeCanMakePayments;

@interface FakePaymentQueue : SKPaymentQueue
@end

@implementation FakePaymentQueue

+ (void)setFakeCanMakePayments:(BOOL)fakeValue
{
    fakeCanMakePayments = fakeValue;
}

+ (BOOL)canMakePayments
{
    return fakeCanMakePayments;
}

@end

Strictly speaking, this isn't a "mock object" -- it's a "fake object." The difference is that a mock object verifies how it's called. A fake object just provides stubbed results.

Now let's create a testing subclass of the original class we want to test.

@interface TestingSubclass : OriginalClass
@end

@implementation TestingSubclass

+ (Class)paymentQueueClass
{
    return [FakePaymentQueue class];
}

@end

So you see, this replaces SKPaymentQueue with FakePaymentQueue. Your tests can now run against TestingSubclass.

Balzac answered 8/12, 2011 at 6:41 Comment(4)
What is the class under test? That is, are you writing a test against SKPaymentQueue, or against a different class that calls SKPaymentQueue?Balzac
Hey Reid; thanks for your kindly reply. Do you mean we need to implement another paymentQueueClass in our testing class? Or hack original framework method category (UnitTests)? Seems I don't fully understand your solution, could you give me more tips, thanks very much.Linton
My testing class is against a different class that calls SKPaymentQueue?Linton
Yes, you are right, we can use fake method for this kind of tough issue.Linton

© 2022 - 2024 — McMap. All rights reserved.