Unit Testing View Controllers and Asynchronous APIs

I have an application that I’m working on that uses Parse (a service that lets you persist objects in the cloud, perform queries, etc.).    Calls to the Parse APIs are initiated from the main thread and dispatched to the global queue, thus  completion of these calls happens asynchronously.

I wanted to write some unit tests to verify the functionality of my methods that interface with Parse.

Referencing the View Controller

My initial view controller is called EntryViewController.   This view controller instances a class called ParseConnection which serves as a model for all my Parse interactions.    All calls to methods in ParseConnection from EntryViewController are dispatched to the global queue.  Within ParseConnection, all the API calls are made synchronously in this background queue.

My first challenge was how to get a reference from my unit tests to EntryViewController.   I wanted to do this from the instance setUp method in my test suite class called ContactsOnlineBasicUnitTests, and store this reference in a property.   This setUp method is called before each test case is run.


@interface  ContactsOnlineBasicUnitTests ()

@property (strong, nonatomic) EntryViewController *entryViewController;

@end

Finding my View Controller via rootViewController

One way to find my reference to EntryViewController was inspired by an unrelated post about UIActionSheet.

One first finds the application’s key window and uses that reference to find the rootViewController which provides the content view of the window. This rootViewController has a property called childViewControllers which is an array of the view controllers that are its children in the view controller hierarchy. Our EntryViewController will be among the children.

    // EntryViewController is a child view controller of our rootViewController
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    UIViewController *rootViewController = [keyWindow rootViewController];
    NSArray *childViewControllersArr = rootViewController.childViewControllers;

    for (UIViewController *vc in childViewControllersArr) {
        if( [vc isMemberOfClass:[EntryViewController class]]){
            self.entryViewController = (EntryViewController *)vc;
        }
    }

    if(!self.entryViewController){
        STFail(@"Could not find EntryViewController as child of rootViewController");
        return;
    }

This worked just fine, but I ended up settling on a more direct approach.

Sharing a reference using a singleton

I discussed how I used a singleton in my post Singletons and synchronized code.

I created a property called entryViewController in my singleton which I set from the viewDidLoad method in EntryViewController.

// Set a pointer to ourselves in sharedCommonStateSingleton for testing purposes.
    [[CommonStateSingleton sharedCommonStateSingleton] setEntryViewController:self];

Here I am taking advantage of the fact that the application loads before my unit tests are invoked.    In ContactsOnlineBasicUnitTests.m I can then grab the reference from the singleton:

// The app under test is always the first to run in the main thread.  We expect that the first
    // invocation of viewDidLoad in EntryViewController.m will set entryViewController in sharedCommonStateSingleton
    if(![[CommonStateSingleton sharedCommonStateSingleton] entryViewController]){
        STFail(@"entryViewController in sharedCommonStateSingleton not set");
        return;
    }

    else {
        self.entryViewController = [[CommonStateSingleton sharedCommonStateSingleton] entryViewController];
    }

Synchronizing to the end of my application’s initialization

My next challenge was to determine when my application was done with its initialization before I proceeded with any test cases.   This initialization will only occur once for my entire test suite.

In my particular application, I use a property in EntryViewController.m called compareAllDoneFlag.      The application starts its initialization in EntryViewController in the main thread, switches to a background thread to perform some synchronization of data with Parse, and then calls back to the main thread to set compareAllDoneFlag.

Control of the main thread begins in the application, but then switches to the unit test once the Parse synchronization is underway.   I needed a way for the unit test to essentially poll on the compareAllDoneFlag in the setUp method before a test case was invoked.

I found the answer in the post How do you unit test asynchronous iOS applications?   The solution was to use NSRunLoop.   My implementation was slightly different than described in the post.  I am using the runUntilDate method to advance time in the unit test, essentially relinquishing control of the main thread for a minimum specified time.


 // Wait for automatic compares to complete
- (BOOL)waitForCompareDoneWithTimeoutSecs:(NSUInteger)timeoutSec
                    errStr:(NSString **)errStr
{
    NSUInteger countSec = 0;

    NSUInteger maxCountSec = 20;  // Default of 20 seconds unless caller overrides

    if(timeoutSec != 0){
        maxCountSec = timeoutSec;
    }

    do {
        if(self.entryViewController.compareAllDoneFlag){
            break;
        }
        countSec ++;

        if(countSec == maxCountSec){
            *errStr = [NSString stringWithFormat:@"Timeout waiting for self.entryViewController.compareAllDoneFlag to be set"];
            return NO;
        }

        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];

    } while (1);

    return YES;
}

Waiting for an asynchronous API call to complete

The technique with NSRunLoop worked above because both the unit test polling and the eventual setting of compareAllDoneFlag occurred in the main thread.

For a lot of my testing I need my test case to wait upon an asynchronous API completion before moving on.    This required a different approach.   I ended up using NSConditionLock.     From the docs this is described as:

“The NSConditionLock class defines objects whose locks can be associated with specific, user-defined conditions. Using an NSConditionLock object, you can ensure that a thread can acquire a lock only if a certain condition is met. Once it has acquired the lock and executed the critical section of code, the thread can relinquish the lock and set the associated condition to something new. “

There is a nice discussion of NSConditionLock in the Thread Programming Guide in the section Using an NSConditionLock Object.

I ran across a post on Stack Overflow that illustrated this approach –  How to unit test asynchronous APIs?.

What about getting information from the asynchronous call?

Blocking my unit test execution until the asynchronous call completes is only half the battle.  I also want to be able to get results from the call (possible error results, data collected for unit test checking, etc.).    To do this, I decided to once again use my singleton to share information between threads, using the locking I do with NSConditionLock to accomplish this safely.

Example call from Unit Test 

In the following example in ContactsOnlineBasicUnitTests.m, we initiate a call to Parse and then wait until we can get a lock to let us know that the call has completed.

// Get Parse records where value matches fieldValue for fieldName.
- (BOOL)parseRecordsForFieldName:(NSString *)fieldName
              matchingFieldValue:(id)fieldValue
                      recordArr:(NSArray **)recordArr
                         errStr:(NSString **)errStr
{
    [self.entryViewController parseGetRecords:self whereFieldName:fieldName matchesFieldValue:fieldValue];

    // Synchronize to the completion of Parse method which occurs in another thread.
    BOOL parseSuccess;
    NSError *error = nil;

    id condLock = [[CommonStateSingleton sharedCommonStateSingleton] unitTestConditionLockObj];

    [condLock lockWhenCondition:UNIT_TEST_CHECKPOINT_PARSE_GET_RECORDS_DONE];

    parseSuccess = [[CommonStateSingleton sharedCommonStateSingleton] unitTestParseSuccess];
    if(!parseSuccess){
        error = [[[CommonStateSingleton sharedCommonStateSingleton] unitTestParseError] copy];
    }
    else {
        *recordArr = [[[CommonStateSingleton sharedCommonStateSingleton] unitTestParseResultArr] copy];
    }

    [condLock unlockWithCondition:UNIT_TEST_CHECKPOINT_NONE];

    if(!parseSuccess){
        NSString *remoteErrStr = [[[error.userInfo objectForKey:NSUnderlyingErrorKey] userInfo] objectForKey:@"error"];
        *errStr = [NSString stringWithFormat:@"Failed parseRecordsForFieldName. Description:%@  UnderlyingError Description:%@",error.localizedDescription,remoteErrStr];
        return NO;
    }

    return YES;
}

In the example above, I am using unitTestConditionLockObj from my singleton which is an instance of NSConditionLock initialized with UNIT_TEST_CHECKPOINT_NONE.

I am conveying success/fail status with unitTestParseSuccess and a possible NSError object with unitTestParseError.   Records from Parse are conveyed via unitTestParseResultArr.

This method blocks until a lock can be obtained with the status UNIT_TEST_CHECKPOINT_PARSE_GET_RECORDS_DONE  (the asynchronous call to Parse completes).

Example implementation of API call in background thread

In the following example in EntryViewController.m, we call parseGetRecords:whereFieldName:matchesFieldValue:recordArr:error:
on self.parseConnection which will make an API call out to Parse to fetch some records.


// Fetch Parse records.
- (void)parseGetRecords:(id)sender
         whereFieldName:(NSString *)fieldName
      matchesFieldValue:(id)fieldValue
{
    // All Parse API calls need to be invoked in global queue
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // Provide a unit test lock around call to parseEditedContact
        id condLock = [[CommonStateSingleton sharedCommonStateSingleton] unitTestConditionLockObj];

        // This lock should always be available when we try it, or else we have a programming design error.
        if(![condLock tryLockWhenCondition:UNIT_TEST_CHECKPOINT_NONE]){
            EXCEPTION_LOG("Unable to get lock for condition = UNIT_TEST_CHECKPOINT_NONE")
        }

        NSError *error = nil;
        NSArray *recordArr = nil;

        // Failure
        if(![self.parseConnection parseGetRecords:sender whereFieldName:fieldName matchesFieldValue:fieldValue recordArr:&recordArr error:&error]){
            [[CommonStateSingleton sharedCommonStateSingleton] setUnitTestParseSuccess:NO];
            [[CommonStateSingleton sharedCommonStateSingleton] setUnitTestParseError:error];
            [[CommonStateSingleton sharedCommonStateSingleton] setUnitTestParseResultArr:nil];
        }

        // Success
        else {
            [[CommonStateSingleton sharedCommonStateSingleton] setUnitTestParseSuccess:YES];
            [[CommonStateSingleton sharedCommonStateSingleton] setUnitTestParseError:nil];
            [[CommonStateSingleton sharedCommonStateSingleton] setUnitTestParseResultArr:recordArr];
        }

        [condLock unlockWithCondition:UNIT_TEST_CHECKPOINT_PARSE_GET_RECORDS_DONE];
    });

}

This method should never block, that is to say that the lock should always be available. We trigger an exception if this is not the case (I use a macro for this purpose, so that I can also send a report for analytics). When this method completes, we relinquish the lock with the condition UNIT_TEST_CHECKPOINT_PARSE_GET_RECORDS_DONE to unblock the unit test.

Advertisements

3 responses to “Unit Testing View Controllers and Asynchronous APIs

  1. Pingback: Functional Coverage and Coverpoints | Finalize.com: My journey with iOS and other code adventures

  2. I wanted to share an update. In the section Sharing a reference using a singleton I mentioned “Here I am taking advantage of the fact that the application loads before my unit tests are invoked. ”

    If you have just a single view controller, then this works just fine. I eventually added a couple view controllers in front of the one that I wanted to get a reference to. I found that my unit test setup was looking for the reference before the view controller of interest was loaded.

    My fix was to wrap the code in a poll loop:

    // Wait up to maxCountSec seconds for EntryViewController to become active and set a reference to itself
    // in CommonStateSingleton.
    NSUInteger countSec = 0;
    NSUInteger maxCountSec = 5;
    do {
    if([[CommonStateSingleton sharedCommonStateSingleton] entryViewController]){
    break;
    }

    if(countSec == maxCountSec){
    EXCEPTION_LOG(“Timeout waiting for entryViewController in CommonStateSingleton to be set”)
    }

    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];

    countSec ++;
    } while (1);

    self.entryViewController = [[CommonStateSingleton sharedCommonStateSingleton] entryViewController];

    Like

  3. Pingback: The development of Contacts2Web | Finalize.com: My journey with iOS and other code adventures

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s