Unit testing async network calls in Swift
Asynchronous unit testing in Swift
You have probably written code with a NSURLSessionDataTask that notifies a delegate when the data is received. How do you write a unit test for that?
Introduction
Let’s stub out some typical code. Here a an API function that takes perhaps a REST endpoint and a delegate that receives a Thing instance. I use a NSURLSessionDataTask because I’m expecting, well, data (as JSON). I’m not showing the gory details of parsing the JSON since that’s not my point here. BTW., it’s not very difficult to parse. The idea is that a Thing is instantiated and the delegate is notified.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func getThing(url:String, delegate:ThingDelegate) { //set up NSURLSession and request... let task : NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in if let e = error { println("Error: \(e.localizedDescription)") } var jsonError:NSError? if let json = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &jsonError) as? NSDictionary { if let e = jsonError { println("Error parsing json: \(e.localizedDescription)") } else { parse the JSON to instantiate a thing... delegate.didReceiveWhatever(thing) |
Testing
So, how do you write a unit test for this kind of code? The API call does not return anything to pass into XCTAssertTrue or siblings. Wouldn’t it be nice if you can make the network API call and wait – with a timeout of course – for a response?
Previously, you’d have to use semaphores, a spin loop, or something similar. Since this is such a common scenario, Apple gave us XCTestExpectation in XCode 6. (Actually, it’s a category in XCTextCase+AsynchronousTesting.)
Here is a simple usage example. I have an instance variable of type XCTestExpectation because I need it in the delegate callback in addition to the test function. I simply instantiate it, make the network call, then call one of the new wait functions. In this case waitForExpectationsWithTimeout. When the delegate is notified, I fulfill the expectation. If you don’t, the test will fail after the timeout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var expectation:XCTestExpectation? func testExample() { expectation = self.expectationWithDescription("asynchronous request") Networkclass.getThing("http://api.things.com/someid", delegate: self) self.waitForExpectationsWithTimeout(10.0, handler:nil) } func didReceiveWhatever(thing:Thing) { expectation?.fulfill() } |
Summary
Simple huh? Take a look at the documentation for a few variations.