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!