Asynchronous Unit Testing in Xcode 6

Last year I described a method to implement asynchronous unit testing in Xcode 5.

Let’s remind ourselves of the problem with asynchronous unit testing. Many APIs on the iOS platform themselves are asynchronous. They have use callback invocations to signal when they’re completed, and these may run in different queues. They may make network requests or write to the local file system. These can be time-consuming tasks that need to run in the background. This creates a problem because tests themselves run synchronously. So our tests need to wait until they are notified of when the running task has completed.

I proposed a method that entailed setting a boolean flag in the unit test and looping in a while() loop until the flag was set to false, allowing the test to complete properly. This method worked most of the time but I have never been happy with it, regarding it as a bit of a kludge. In that blog post I concluded:

I still have my reservations about this technique, and I’m still looking for the perfect solution for asynchronous unit testing in Xcode. You would think that Apple might have provided a solution in XCTest, perhaps similar to the implementation in GHUnit.

Here’s what the Objective-C version of a bare bones example asynchronous unit test in Xcode 5 using the old method looks like:

- (void)testSaveAndCreateDocument {
    NSURL *url = ...; // URL to file
    UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:url];
 
    // Set the flag to YES
    __block BOOL waitingForBlock = YES;
 
    // Call the asynchronous method with completion block
    [document saveToURL:document.fileURL
        forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
            // Set the flag to NO to break the loop
            waitingForBlock = NO;
            // Assert the truth
            STAssertTrue(success, @"Should have been success!");
        }];
 
    // Run the loop
    while(waitingForBlock) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
    }
}

In fact, because I was re-using the same pattern in many of my tests, I converted parts of it to a Macro that had to be included in each header file. Also, I noted that under some conditions the test didn’t complete properly.

Well, the good news is that, less than a year later, Apple have delivered a means to implement asynchronous unit tests in an intelligent and offcially supported way. Furthermore, not only have they given us a new version of Xcode 6 (still in beta at the time of writing) with this new unit testing framework, but they have also delivered a brand new programming language, Swift. I’ve spent some time over the last few weeks converting a hefty chunk of Objective-C code to Swift, and in converting my Unit Tests to the XCTest framework I implemented Apple’s new methods for asynchronous unit testing. From now on, all of my iOS coding is going to be done in Swift, so the examples below will be in Swift, too.

So, how does it work? In Xcode 6 Apple have added some extensions to the XCTestCase class, and I’m going to focus on two of them:

// expectationWithDescription
func expectationWithDescription(description: String!) -> XCTestExpectation!

// waitForExpectationsWithTimeout
func waitForExpectationsWithTimeout(timeout: NSTimeInterval, handler handlerOrNil: XCWaitCompletionHandler!)

There’s also a new class, XCTestExpectation which has one method:


class XCTestExpectation : NSObject {
    func fulfill()
}

Basically, you declare an “expectation” in your unit test, and loop in a wait loop waiting for the expectation to be fulfilled in your code. It’s the same pattern as before, but with more options. Here’s the old Objective-C code converted to Swift using the new framework:

    func testSaveAndCreateDocument() {
        let url = NSURL.URLWithString("path-to-file")
        let document = UIManagedDocument(fileURL: url)

        // Declare our expectation
        let readyExpectation = expectationWithDescription("ready")

        // Call the asynchronous method with completion handler
        document.saveToURL(url, forSaveOperation: UIDocumentSaveOperation.ForCreating, completionHandler: { success in
            // Perform our tests...
            XCTAssertTrue(success, "saveToURL failed")

            // And fulfill the expectation...
            readyExpectation.fulfill()
        })
        
        // Loop until the expectation is fulfilled
        waitForExpectationsWithTimeout(5, { error in
            XCTAssertNil(error, "Error")
        })
    }

In line 6 we instantiate a new instance of XCTestExpectation, named readyExpectation. We give it a simple description for convenience, “ready”. This will be displayed in the test log to help diagnose failures. It is also possible to set more than one expectation as a condition. Then in line 9 we make the call to the code that needs to be tested. In the completion handler, after making our tests, we call the method fulfill() on the expectation. This is equivalent to setting the flag to false in our earlier Objective-C implementation.

The last block of clode starting at line 18 runs the run loop while handling events until all expectations are fulfilled or the timeout is reached. I set the timeout to 5 seconds to be on the safe side.

And that’s about it. There’s more you can do with the new additions to the unit test framework, such as key-value observing, and performance metrics, but the above should be sufficient to get going. Finally, we have a proper framework for Asynchronous Unit Testing in Xcode!

Begin typing your search term above and press enter to search. Press ESC to cancel.