Stubbing a method using a completion block in an iOS Kiwi test case

Kiwi is a great BDD (Behaviour-driven development) testing framework for iOS. The structure of a Kiwi test case involves specifying expected outcomes and behaviours for a given context or situation. The emphasis is on testing the “what” not the “how”.

I’ve been using Kiwi a fair bit recently, when I ran into a situation I wanted to test that wasn’t as clearly documented as I would’ve liked, so I thought I’d provide the solution I used here. I felt it best to dive straight into some example code to better explain the situation:

Class being tested: ClientOfApiService

Method being tested: doSomeActionThatInvolvesApiService:

Class dependencies: ApiService

To properly test the behaviour of ClientOfApiService in isolation, we need to provide a mock of the dependency ApiService; failing to do this would mean relying on external behaviour which could potentially lead to inconsistent/invalid results.

ClientOfApiService.h

typedef void(^ActionCompletionBlock)(BOOL success);

@class ApiService;

@interface ClientOfApiService : NSObject

@property(strong, nonatomic) ApiService *apiService;

/*
 * Performs some action that requires the result of an API to indicate
 * success or failure.
 */
-(void) doSomeActionThatInvolvesApiService:(ActionCompletionBlock) completion;

ClientOfApiService.m

-(void) doSomeActionThatInvolvesApiService:(ActionCompletionBlock) completion;
{
    [self.apiService fetchTestNames:^(NSArray *testNames, NSError *error) {

        if (error)
        {
            completion(NO);
            return;
        }

        // Success is arbitrarily a name array of size 2
        BOOL success = (testNames != nil && [testNames count] == 2);

        completion(success);
    }];
}

ApiService.h

typedef void(^TestNamesCompletionBlock)(NSArray *testNames, NSError *error);

@interface ApiService : NSObject

/*
 * Fetches some test names from the API and calls the specified completion
 * block when a result is received.
 */
-(void) fetchTestNames:(TestNamesCompletionBlock) completion;

So, the objectives are:

  1. Stub the fetchTestNames method to use dummy data so that it does not have to make a real API call.
  2. On top of stubbing the fetchTestNames method call, we want the method to capture the TestNamesCompletionBlock parameter so that it can properly call back by executing the block with the dummy data.
  3. In our test case, verify that the outcome of the doSomeActionThatInvolvesApiService method is correct given the stubbed data used.

I’ll end this post with an example of a Kiwi test case that achieves the above objectives:

#import <Kiwi/Kiwi.h>
#import "ClientOfApiService.h"
#import "ApiService.h"

SPEC_BEGIN(ClientOfApiServiceSpec)

describe(@"ClientOfApiService", ^{
    
    // system under test (class being tested)
    __block ClientOfApiService *sut;
    __block id apiService;
    
    // Runs before each test
    beforeEach(^{
        sut = [[ClientOfApiService alloc] init];
        apiService = [KWMock mockForClass:[ApiService class]];
        
        // Inject the mocked apiService into the ClientOfApiService instance
        sut.apiService = apiService;
    });
    
    // Runs after each test
    afterEach(^{
        sut = nil;
        apiService = nil;
    });
    
    it(@"ClientOfApiService instance should exist", ^{
        [sut shouldNotBeNil];
    });
    
    // Given a particular context
    context(@"When testing doSomeActionThatInvolvesApiService with some dummy data", ^{
        
        // Runs before each test
        beforeEach(^{
            
            // OBJECTIVE 1. Stub the mocked ApiService to return an array of 2 names
            // We stub using a block so that we can capture that parameters passed to the
            // method we are stubbing. The parameters can be accessed in the params array
            // object
            [apiService stub:@selector(fetchTestNames:) withBlock:^id(NSArray *params) {
                
                // OBJECTIVE 2. Capture the TestNamesCompletionBlock parameter passed in so
                // that we can call back with the dummy data. The completion block parameter
                // is the only parameter of the method, so we access it at index 0.
                TestNamesCompletionBlock completionBlock = [params objectAtIndex:0];
                
                // Dummy data with 2 names
                NSArray *dummyArray = [NSArray arrayWithObjects:@"John", @"Matt", nil];
                
                // call back by executing the completion block parameter
                completionBlock(dummyArray, nil);
                
                // the test framework requires the block to return something
                return nil;
            }];
            
        });
        
        // Verify that some condition is met. For test purposes,
        // success is abitrarily defined as the ApiService returning
        // exactly 2 names.
        it(@"should result in success being returned", ^{
            [sut doSomeActionThatInvolvesApiService:^(BOOL success) {
                
                // OBJECTIVE 3. verify that the correct result was returned
                // given the dummy data used.
                [[theValue(success) should] equal:theValue(YES)];
            }];
        });
        
    });
});

SPEC_END

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s