Testing Async code

A common problem every developer encounters when adding tests to their software is how to test asynchronous code. Why is it a problem? It's because the flow of a test is expecting synchronous code.
Let's consider, for example, testing a file loader that loads the content of a JSON file in an asynchronous way:

class FileLoader {
func loadContent(fromFilename: String,
onSuccess: @escaping (Any) -> Void) {
guard let path = Bundle.main.path(forResource: fromFilename,
ofType: "json") else {
return
}

DispatchQueue.global(qos: .background).async {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path),
options: .alwaysMapped) else {
print("Error: Couldn't decode data")
return

}

guard let decodedJSON = try? JSONSerialization.jsonObject(
with: data, options: []) else {
print("Error: Couldn't decode data")
return
}
DispatchQueue.main.async {
onSuccess(decodedJSON)
}
}
}
}

If we want to test it, we could write a test such as this:

func testLoadingFile() {
let fileLoader = FileLoader()

fileLoader.loadContent(fromFilename: "FixturePlayers") { result in
guard let array = result as? [[String: Any]] else {
return XCTFail()
}
XCTAssertEqual(5, array.count)
}
}

If we run it, the test is immediately green, but it is a false positive; the code with the assertion is never run. We can verify this by changing the number of players in the assert to 10 for example, and we see that the tests are still passing. The reason for this is because the test runs in a synchronous way, and after calling the loadContent function, it finishes without waiting for the closure to be called. To execute this kind of test, Xcode introduced the concept of expectation, which is a way for the execution to wait until something fulfills the expectation or a timeout arrives:

func testLoadingFile() {
let fileLoader = FileLoader()

let expectation = self.expectation(description: "Loading")

fileLoader.loadContent(fromFilename: "FixturePlayers") { result in
defer {
expectation.fulfill()
}
guard let array = result as? [[String: Any]] else {
return XCTFail()
}
XCTAssertEqual(5, array.count)
}

waitForExpectations(timeout: 5)
}

This is a useful addition that Apple made in Version 6 of Xcode, which makes testing asynchronous code straightforward, giving us no excuses for not testing it.