Upcoming and OnDemand Webinars View full list

Asynchronous Testing With Xcode 6

Sean McCune

In 2013, Apple shipped a revamped testing framework in Xcode called XCTest,
and there was much rejoicing. The old framework hadn’t been updated in years,
and a number of third-party testing tools and frameworks had sprung up to
provide new features and capabilities. It was good to see the built-in
tools getting some love again, and this year, Apple is shipping a few features
with Xcode 6 that were missing from last year’s update. One I’m particularly
glad to see is support for asynchronous testing.

If we have a test that has to kick off an asynchronous task, whether it runs in
another thread or in the main thread’s runloop, how do we test it?

Consider a web request. We could kick off a web request and pass in a completion
block, then make our test assertions, either in the completion handler or
not. However, because the web request hasn’t even been made yet, much less a
response received nor has our completion block been called, our test method is
going to exit before the assertions are ever tested.

Let’s look at
a test for a class that downloads web pages. Normally, we wouldn’t want to make
actual web requests in tests. Instead, we’d stub out the requests using some
tool (I’m partial to OHHTTPStubs).
But for the purposes of these examples, we’ll break some rules and make real web
requests.

We can give the class under test a URL and completion handler block,
and it will download the page and call the block, passing in a string
containing the web page or an empty string if a failure occurs. It’s not a
great API, but again, we’re breaking some rules. However, the test code below
is never going to fail. The test method will return without giving the
completionHandler block a chance to be called.

    - (void)testCodeYouShouldNeverWrite
    {
	    __block NSString *pageContents = nil;

        [self.pageLoader requestUrl:@"http://bignerdranch.com"
                  completionHandler:^(NSString *page) {

            NSLog(@"The web page is %ld bytes long.", page.length);
			pageContents = page;
			// Test method ends before this test assertion is called
            XCTAssert(pageContents.length > 0);
        }];

		// Nothing prevents the test method from returning before
		// completionHandler is called.
    }

Before Xcode 6’s version of XCTest, just using what comes in the tin with
Xcode, we could sit and spin in a while loop that calls the main thread’s
run loop until the response arrives or some timeout period has elapsed.
Here’s working test code, the old way.

    - (void)testAsyncTheOldWay
    {
        NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
        __block BOOL responseHasArrived = NO;

        [self.pageLoader requestUrl:@"http://bignerdranch.com"
                  completionHandler:^(NSString *page) {

            NSLog(@"The web page is %ld bytes long.", page.length);
            responseHasArrived = YES;
            XCTAssert(page.length > 0);
        }];

        while (responseHasArrived == NO && ([timeoutDate timeIntervalSinceNow] > 0)) {
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
        }

        if (responseHasArrived == NO) {
            XCTFail(@"Test timed out");
        }
    }

The while loop runs the main thread’s run loop for 10 milliseconds at a time
until the response arrives, or until 5 seconds elapses without it having
arrived. This is serviceable. It’s not terrible. It is not the end of the
software development world—but it’s not great.

Now there’s a better way.

High Expectations

With Xcode 6, Apple has added test expectations to the XCTest framework in the
form of the XCTestExpectation class. When we instantiate a test expectation,
the testing framework expects that it will be fulfilled at some point in
the future. Our test code fulfills the expectation in the completion block
with a call to the XCTestExpectation method fulfill. This takes the place of
setting a flag like responseHasArrived in the previous example. Then we tell
the test framework to wait (with a timeout) for its expectations to be
fulfilled via the XCTestCase method waitForExpectationsWithTimeout:handler:.
If the completion handler is executed within the timeout and calls fulfill,
then all of the test’s expectations will have been fulfilled. If not, then the
test will live a sad, lonely, unfulfilled existence… until it goes out of
scope. And by living a sad, lonely, unfulfilled existence, I mean that the
expectation fails the test upon timing out.

The failed expectation shouldn’t feel so dejected. Remember that a failing
result is not the sign of a bad test; an indeterminate result is. That
expectation can feel pride as it declares failure.

Here’s an example using XCTestExpectation:

    - (void)testWebPageDownload
    {
        XCTestExpectation *expectation =
            [self expectationWithDescription:@"High Expectations"];

        [self.pageLoader requestUrl:@"http://bignerdranch.com"
                  completionHandler:^(NSString *page) {

            NSLog(@"The web page is %ld bytes long.", page.length);
            XCTAssert(page.length > 0);
            [expectation fulfill];
        }];

        [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
            if (error) {
                NSLog(@"Timeout Error: %@", error);
            }
        }];
    }

Create the expectation with a description to make the results more readable.
In the completion block, call [expectation fulfill] to tell the test that
this expectation has, indeed, been fulfilled. Then hang out in the
waitForExpectationsWithTimeout:handler: until the request is sent, the
response arrives and our completion handler is called… or the timeout
occurs.

That’s good ol’ Objective-C, but we can also do it in Apple’s shiny new Swift language.

    func testWebPageDownload() {

        let expectation = expectationWithDescription("Swift Expectations")

        self.pageLoader.requestUrl("http://bignerdranch.com", completion: {
            (page: String?) -> () in
            if let downloadedPage = page {
                XCTAssert(!downloadedPage.isEmpty, "The page is empty")
                expectation.fulfill()
            }
        })

        waitForExpectationsWithTimeout(5.0, handler:nil)
    }

And that’s it. It’s an easy-to-use class for testing asynchronous code.

Can’t get enough info about iOS 8 and Swift? Join us for our Beginning iOS with Swift and Advanced iOS bootcamps.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project