How to stub method block in Kiwi?
Asked Answered
S

1

7

I want to stub a method which takes a block as a parameter using Kiwi. Here is the full explanation with code:

I have a class named TestedClass which has a method testedMethod which dependent on class NetworkClass which calls via AFNetworking to a server, and return its response via block. Translating to code:

@interface TestedClass : NSObject
    -(void)testMethod;
@end

-(void)testMethod
{
    NetworkClass *networkClass = [[NetworkClass alloc] init];

    [networkClass networkMethod:^(id result)
    {
        // code that I want to test according to the block given which I want to stub
        ...
    }];
}



typedef void (^NetworkClassCallback)(id result);

 @interface NetworkClass : NSObject
 -(void)networkMethod:(NetworkClassCallback)handler;
 @end

-(void) networkMethod:(NetworkClassCallback)handler
{
    NSDictionary *params = @{@"param":@", @"value"};
    NSString *requestURL = [NSString stringWithFormat:@"www.someserver.com"];
    AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURLURLWithString:requestURL]];
    NSURLRequest *request = [httpClient requestWithMethod:@"GET" path:requestURL parameters:params];
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
         handler(responseObject);
    }

} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    handler(nil);
}];

[operation start];
}

How can I use Kiwi to stub networkMethod with block in order to unit test testMethod?

UPDATE: Found how to do this in Kiwi, see my answer below.

Sperling answered 16/10, 2013 at 8:49 Comment(0)
S
11

Here is how you do this in Kiwi:

First, you must dependency inject NetworkClass to TestedClass (if it's not clear how, please add a comment and I'll explain; this can be done as a property for simplicity. This is so that you can operate on a mock object for the NetworkClass)

Then your spec, create the mock for the network class and create your class that you want to unit test:

SPEC_BEGIN(TestSpec)

describe(@"describe goes here", ^{
    it(@"should test block", ^{
        NetworkClass *mockNetworkClass = [NetworkClass mock];
        KWCaptureSpy *spy = [mockNetworkClass captureArgument:@selector(networkMethod:) atIndex:0];
        TestedClass testClass = [TestedClass alloc] init];
        testClass.networkClass = mockNetworkClass;
        [testClass testMethod];

        NetworkClassCallback blockToRun = spy.argument;
        blockToRun(nil);

        // add expectations here

    });
});

SPEC_END

To explain what's going on here:

You are creating TestedClass and calling testMethod. However, before that, we are creating something called Spy - its job is to capture the block in the first parameter when networkMethod: is called. Now, it's time to actually execute the block itself.

It's easy to be confused here so I'll emphasize this: the order of calls is important; you first declare the spy, then call the tested method, and only then you're actually calling and executing the block!

This will give you the ability to check what you want as you're the one executing the block.

Hope it helps for other, as it took me quite sometime to understand this flow.

Sperling answered 27/11, 2013 at 11:49 Comment(5)
what if you don't want to expose the TestedClass NetworkClass instance? (it's either a readonly or a private property)? Is this possible?Phospholipide
Is dependency injection required to get this kind of spec to work? The pattern sounds interesting but don't want to take the time right now. Also, is the assignment to *spy supposed to be calling [mockNetworkClass captureArgument: ... (instead of networkClass) or am I missing something?Sadfaced
@Sadfaced Yes you have to supply the mock object - this is done with the assignment: testClass.networkClass = mockNetworkClass, and thanks for the typo ;)Sperling
@AdamWaite sorry for the late reply! In your spec file you can add a category of testedClass and expose only there the property - this way you provide the encapsulation needed while being able to inject that property.Sperling
@Sadfaced FTI, I've just tried to call captureArgument on the real object without mocking and it did execute the block, however, I wouldn't do that as the real implementation will probably also call the block and that might have its side effects.Sperling

© 2022 - 2024 — McMap. All rights reserved.