Lately I’ve found myself writing asynchronous network calls on mobile platforms in the reactive style. I want to share how you can test-drive calls like this for iOS. The following example uses SwiftyJSON for JSON deseriaization and FutureKit as the asynchronous framework surrounding the network call. There are several libraries in iOS that do both of these things, but your structure here will look similar with any of them.
This example is updated to Swift 3.
0. Overall Structure
On the implementation side, you’ll have two components. First, a service will wrap the network call and return a future. Second, some object will call that service on the main thread and respond to the way the result completes (in our case, this will be a ViewController
). So this means we’ll need to mock the result of the network call that the service makes. We’ll also need to test each of the different ways the future can complete to make sure the ViewController
responds the way we would like.
Now let’s look at some code from a sample app about…villains.

1. Unit Testing the Service
We’ll be unit testing the service here with the standard test runner for iOS, XCTest (though you could also use Quick if you prefer), and with matchers from Nimble (although Hamcrest or XCTassert would do fine as well).
As mentioned above, this service wraps the network call itself, which in iOS means calling dataTask()
on your session object, specified as URLSession.shared
. We need to mock that object to ensure that our test asserts on the functionality of our code and not the iOS framework. We also want to stub the response from this call, which is a URLSessionDataTask
on which we call resume()
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import XCTest | |
import Nimble | |
import SwiftyJSON | |
import FutureKit | |
@testable import VillainApp | |
class VillainServiceTest: XCTestCase { | |
var service: VillainService! | |
var mockSession: MockSession! | |
let fakeHTTPResponse = HTTPURLResponse(url: URL(string: "http://www.wearevillains.com/")!, | |
statusCode: 200, httpVersion: nil, headerFields: nil)! | |
override func setUp() { | |
super.setUp() | |
self.continueAfterFailure = false | |
service = VillainService() | |
mockSession = MockSession() | |
service.session = mockSession | |
} | |
func testGetVillainList() { | |
let villainsDict = ["villainCount": 3, | |
"villains":[ | |
["name": "Cruella DeVille", | |
"specialty": "unethical treatment of animals"], | |
["name": "Ursula", | |
"specialty": "fine print"], | |
["name": "Elphaba", | |
"specialty": "faking own death"], | |
] | |
] | |
let villainResponse = VillainResponse.init(fromJSON: JSON(villainsDict)) | |
let villainData: Data = try! JSONSerialization.data(withJSONObject: villainsDict, options: .prettyPrinted) | |
let someResponse = URLResponse(url: URL(string: "http://www.wearevillains.com/")!, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) | |
let future = self.service.getVillainList() | |
mockSession.completionHandler!(villainData, someResponse, nil) | |
future.onSuccess { (successResponse) -> Void in | |
expect(self.mockSession.request?.url?.scheme).to(equal("http")) | |
expect(self.mockSession.request?.url?.host).to(equal("http://www.wearevillains.com")) | |
expect(self.mockSession.request?.url?.path).to(equal("/team")) | |
expect(self.mockSession.request?.httpMethod).to(equal("GET")) | |
expect(successResponse).toEventually(equal(villainResponse)) | |
expect(successResponse.login.expand.absoluteString).to(equal("http://www.wearevillains.com/")) | |
expect(self.mockSession.stubbedDataTask.resumeWasCalled).to(beTrue()) | |
} | |
future.onCancel { | |
fail("Should have successfully obtained villains! Cancelled instead.") | |
} | |
future.onFail { (error) -> Void in | |
print(error) | |
fail("Should have successfully obtained villains! Failed instead.") | |
} | |
expect(future.isCompleted).to(beTrue()) | |
} | |
} |
On line 42, we call the method we’re testing. On line 44, we trigger the response. So session.dataTask()
takes a completion handler closure that accepts data, a response, and an error. When we call that method on our mock, our mock will save off the completion handler that the service passes in. Then we’ll call that completion handler with the data that we want the service to think that the network call returned.
This is what your mocks look like so you can perform those assertions on your session and on the task that your session.dataTask()
call returns:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MockSession: URLSession { | |
var completionHandler: ((Data?, URLResponse?, Error?) -> Void)? | |
var request: URLRequest? | |
let stubbedDataTask: MockURLSessionDataTask = MockURLSessionDataTask() | |
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { | |
self.completionHandler = completionHandler | |
self.request = request | |
return stubbedDataTask | |
} | |
} | |
class MockURLSessionDataTask: URLSessionDataTask { | |
var resumeWasCalled: Bool = false | |
override func resume() { | |
resumeWasCalled = true | |
} | |
} |
2. Implementing the Service
So what does this service look like? It keeps the session as a stored field and calls session.dataTask().
The key piece that differentiates our service from that method, and our service from other services, is the completion handler that we’re passing into that method. That completion handler will know what kind of responses to expect. Our service will make a new Promise of the type of response it expects to receive, and it will parse the response we get to determine how to complete the promise. Then we’ll return a future of that promise, which our caller will use to respond when the promise completes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import FutureKit | |
import SwiftyJSON | |
class VillainService { | |
let url: String | |
var session: URLSession = URLSession.shared | |
func getVillainList() -> Future<VillainResponse> { | |
let promise = Promise<VillainResponse>() | |
var request = URLRequest(url: URL(string: "http://www.wearevillains.com/team")) | |
request.httpMethod = "GET" | |
let task = session.dataTask(with: request) { data, response, error in | |
if error != nil { | |
promise.completeWithFail(error!) | |
} else { | |
do { | |
if let convertedJsonIntoDict = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary { | |
let dataAsJson = JSON(convertedJsonIntoDict) | |
promise.completeWithSuccess(APIRootResourceLinks.init(fromJSON: dataAsJson)!) | |
} | |
} catch let error as NSError { | |
promise.completeWithFail(error) | |
} | |
} | |
task.resume() | |
return promise.future | |
} | |
} | |
extension Villain : JSONDeserializable { | |
init?(fromJSON json: JSON) { | |
guard let name = json["name"].string else { return nil } | |
guard let specialty = json["specialty"].string else { return nil } | |
} | |
} | |
extension VillainResponse : JSONDeserializable { | |
init?(fromJSON json: JSON) { | |
guard let | |
villainCount = json["villainCount"].int, | |
let villains = json["villains"].array { | |
for (index, vill) in villains.enumerated() { | |
if let villain = Villain(fromJson: json["villains"][index]) { | |
villains.append(villain) | |
} | |
} | |
} else { return nil } | |
count = villainCount | |
villains = villains | |
} | |
} |
3. Unit Testing the ViewController
So we have our service that makes a call to get a list of villains. Suppose we want to show that list of villains on a screen when it comes back. So we’re going to make a VillainsMasterViewController to see the villains. But first, let’s look at the structure of our unit tests for the ViewController’s responses when our promise completes.
You’ll see in this example test some of the other important concepts we have discussed in the past around testing iOS apps, namely mocking dependencies and presenting ViewControllers in tests. If you are interested or you need a refresher, check out these posts:
Test-Driven iOS: Mocking Dependencies
Test-Driven iOS: Using Segues (covers UI unit testing)
On to the code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import FutureKit | |
import XCTest | |
import Nimble | |
@testable import VillainApp | |
class VillainsMasterViewControllerTest: XCTestCase { | |
var viewController: VillainsMasterViewController! | |
var mockVillainService: MockVillainService! | |
var somePromise: Promise<VillainResponse>? | |
class MockVillainService: VillainService { | |
var stubbedPromise: Promise<VillainResponse>? | |
override func getVillainList() -> Future<VillainResponse> { | |
return stubbedPromise.future | |
} | |
} | |
override func setUp() { | |
super.setUp() | |
somePromise = Promise<VillainResponse>() | |
mockVillainService = MockVillainService() | |
mockVillainService.stubbedPromise = somePromise | |
} | |
… | |
func testSuccessfulCallDisplaysVillains() { | |
let villainsDict = ["villainCount": 3, | |
"villains":[ | |
["name": "Cruella DeVille", | |
"specialty": "unethical treatment of animals"], | |
["name": "Ursula", | |
"specialty": "fine print"], | |
["name": "Elphaba", | |
"specialty": "faking own death"], | |
] | |
] | |
let villainsResponse = VillainResponse.init(fromJSON: JSON(villainsDict)) | |
villainsController.loadFromStoryboard(mockVillainService) | |
assertThat(villainsController.view, present()) | |
let window = UIWindow() | |
window.rootViewController = controller | |
window.makeKeyAndVisible() | |
controller.didTapLoginButton(NSObject()) | |
expect(self.somePromise?.isCompleted).toNot(beTrue()) | |
somePromise?.completeWithSuccess(villainsResponse!) | |
expect(self.somePromise?.isCompleted).to(beTrue()) | |
//assert that the villain list is populated | |
window.resignKey() | |
window.isHidden = true | |
} | |
} |
4. Implementing the ViewController Response
This part of the asynchronous call structure feels really nice. We make the call to our VillainService in our ViewController. The service returns a future, and we chain our response blocks onto that future in the ViewController so they will be triggered when the promise completes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import FutureKit | |
import UIKit | |
class VillainsMasterViewController: UIViewController { | |
let villainService: VillainService? | |
… | |
func viewDidLoad() { | |
villainService?.getVillainList() | |
.onSuccess() { (apiRootResourceLinks) in | |
//display villain information in view | |
}.onCancel { | |
//tell the user that the villain call got cancelled | |
}.onFail { (error) in | |
//show the villain app error page | |
} | |
} | |
… | |
} |
Testing your asynchronous network calls in iOS requires a solid understanding of how to isolate the objects under test; once that is done, you can manipulate the network responses and call completions with as little as a line of code to test your implementation. These tests run independently of your actual network call to give you an idea of the state of the mobile code in isolation. Most importantly, tests like this can help you identify when you have mishandled a case while adding code or refactoring.