How to unit test completion blocks

Blocks have become quite pervasive in iOS. iOS engineers around here are getting pretty comfortable working with them, and are using them more and more. However, they present some problems for unit testing:

Blocks are typically used asynchronously 
For example, run when a network API request completes. This sort of delayed execution is definitely bad for unit tests. Unit tests should run fast, otherwise we won’t run them frequently.

Blocks are lexically scoped
This means that variables are captured at the time that the block is defined, not when it is executed. This is one of the wonderful things about blocks, but further complicates unit testing them.

Blocks allow near proximity definition
Block are typically defined inline where needed. This is great for making code more readable, placing the code in near proximity to where it is used. However, using the common technique of extracting the code to a separate method or function for testing undoes this desirable attribute.

So how do we test blocks? Literally every other blog post I’ve read recommends using some sort of delay mechanism to wait for the block to actually run. This might be acceptable for integration tests, but would likely cause unit tests to take too long to run. There are also test frameworks like OHHTTPStubs that can help in specific situations, but I’m looking for a general solution to this problem.

I’d love to have support built-into whichever testing framework is being used, but so far I have not found any that do this yet (although some come very close). After quite a bit of thought and experimentation on this, I think the best general solution for testing completion blocks is to use the Michael Feathers “Subclass and Override” pattern. This approach can be done regardless of the unit testing framework being used. I will demonstrate this with an example using AFNetworking. The following is the (simplified) product code needing to be unit tested:

@implementation MyClass { //in product code
...
- (void)sendAsyncRequest:(NSURLRequest *)urlRequest {
    AFJSONRequestOperation *operation = 
       [AFJSONRequestOperation JSONRequestOperationWithRequest:urlRequest
       success:^(NSURLRequest *request,NSHTTPURLResponse *response,id JSON) 
    {
        NSLog(@"Success: returned object = %@",JSON);
        //do whatever else needs to be done...
    }failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) 
    {
        NSLog(@"Failed async request");
        //do whatever else needs to be done to handle the failure...
    }];
    [operation start];
}
...
@end

The above code appears to be doing 2 things:

  1. It creates an AFJSONRequestOperation object.
  2. It calls the start method on that object

But to completely unit test this method, we’ll need to verify 7 things:

  1. An AFJSONRequestOperation block is created successfully.
  2. The urlRequest argument is used in creating the AFJSONRequestOperation object.
  3. The success block is used in creating the AFJSONRequestOperation object.
  4. The success block performs as expected.
  5. The failure block is used in creating the AFJSONRequestOperation object.
  6. The failure block performs as expected.
  7. The start method is called on the AFJSONRequestOperation object.

This seems a bit daunting, or at the least like a lot of work for so little a method. But I think that we can establish a pattern for doing this, and maybe some code snippets or reusable code to simplify testing this type of code going forward. So the work here should be mostly a learning effort, and then testing code with completion blocks will be easier going forward.

So let’s focus #4, how to test that the success block executes as expected. The first thing that we’ll need to do is to extract the call to the class method into a separate method. This will give us access to the success and failure blocks that are being defined:

- (void)sendAsyncRequest:(NSURLRequest *)urlRequest {
    AFJSONRequestOperation *operation = 
       [self createOurJSONRequestOperationWithRequest:urlRequest
       success:^(NSURLRequest *request,NSHTTPURLResponse *response,id JSON) 
    {
        NSLog(@"Success: returned object = %@",JSON);
        //do whatever else needs to be done...
    }failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) 
    {
        NSLog(@"Failed async request");
        //do whatever else needs to be done to handle the failure...
    }];
    [operation start];
}

- (AFJSONRequestOperation *)createOurAFJSONRequestOperation:(NSURLRequest *)request
                    success:(AFRequestSuccessBlock)success
                    failure:(AFRequestFailureBlock)failure {
    return [AFJSONRequestOperation JSONRequestOperationWithRequest:request 
            success:success 
            failure:failure];
}

Next we subclass our object under test (MyClass) and override the newly extracted method:

@interface MyClassForTesting : MyClass

@property BOOL createOurAFJSONRequestOperationWasCalled;
@property BOOL fireSuccessBlock;
@property BOOL fireFailureBlock;

- (AFJSONRequestOperation *)createOurAFJSONRequestOperation:(NSURLRequest *)request
           withSuccessBlock:(void(^)(NSURLRequest *urlRequest,NSHTTPURLResponse *urlResponse,id JSON))success
           withFailureBlock:(void(^)(NSURLRequest *urlRequest,NSHTTPURLResponse *urlResponse,NSError *error,id JSON))failure;
@end

@implementation MyClassForTesting

- (AFJSONRequestOperation *)createOurAFJSONRequestOperation:(NSURLRequest *)request
                            success:(void(^)(NSURLRequest *urlRequest,NSHTTPURLResponse *urlResponse,id JSON))success
                            failure:(void(^)(NSURLRequest *urlRequest,NSHTTPURLResponse *urlResponse,NSError *error,id JSON))failure {

    NSURLRequest *urlRequest = nil;         //dummy request
    NSHTTPURLResponse *urlResponse = nil;   //dummy response
    NSError *error = nil;
    
    //Caveat: if we fire immediately, environment will be different than that
    //        of the product, since product has delay before blocks fired.
    if(self.fireSuccessBlock) {
        success(urlRequest,urlResponse,self.JSON);
    }
    if(self.fireFailureBlock) {
        failure(urlRequest,urlResponse,error,self.JSON);
    }
    return nil;
}
@end

So now in this code, we are overriding the extracted method, and using a flag to determine whether or not to fire the passed success and/or failure completion blocks.

One nice thing about this approach is that the only change to the product code is the change to refactor and extract the createOurAFJSONRequestOperation:success:failure: method. All the other code to subclass and override the method resides in our test files, and is not included in the product code.

Here’s an example of a Kiwi test using the subclassed MyObject. In this case, I’m using literals to define the JSON data of interest (weather data) to fire a completion block that will parse this data and assign it to a temperature property:

    __block MyClassForTesting * sut = nil;
    ...
    it(@"passes a success block that sets temperature", ^{
        self.sut = [[MyClassForTesting alloc]init];
        sut.JSON = @{ @"current_observation" : @{ @"temp_f" : @76.5 } };
        sut.fireSuccessBlock = YES;
        [sut sendWeatherServiceRequest:kTEST_ZIPCODE];
        float temp = sut.weather.temperature;
        [[theValue(temp) should] equal:76.5 withDelta:0.01];
    });
    ...

So that’s a general approach that we can use to intercept method calls on the object under test. I admit that it appears messy as shown. It should be fairly easy to wrap all this up into a generic reusable class to encapsulate and hide all the gory details. This would make the test code easier to create, and more importantly, easier to read and understand.

One thought on “How to unit test completion blocks

  1. I’ve updated this code several times. This latest code uses subclass and override as described by Michael Feathers in his Legacy Code book. I’ve also created issue #200 on the Kiwi site requesting to provide built-in support for doing this sort of thing in Kiwi.

Leave a Reply