Asynchronous Testing With Xcode 6

Sean McCune's Headshot
Sean McCune iOS Swift

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.

Tags: iOS 8

Are you looking for a partner in developing an iOS app? Do you want to accelerate your learning? Sean McCune and the rest of the nerds can teach you the latest and greatest in iOS development.

Recent Comments

comments powered by Disqus
  • Jade Hill's Headshot
    Jade Hill

    RELATED

    Video: Cross-Platform Game Templates in Xcode 8

    It’s been possible to write cross-platform SpriteKit code, but you had to build all the boilerplate yourself. The new Xcode 8 Beta 3 includes a new project template for cross-platform games, saving many hours of repetitive setup. In this video, Steve Sparks walks through the structure of the template and shows you how to start creating your own.