Swift makes closures so pleasant to read, you may forget they’re there. We have a good start on testing the data task completion handler. But inside that closure, there’s another closure hiding. It’s DispatchQueue.main.async. The part in braces is a closure, scheduled to run on the main thread.
In real life, Cocoa Touch calls the data task completion handler on a background thread. This lets us parse the response without causing the UI to stutter. We can use different approaches for this. I’ve chosen a simple strategy of keeping the decoding in the background, then saving the results on the main thread because that’s what operates the UI.
But be aware that test code executes on the main thread. And when any code schedules a closure for asynchronous execution, life gets tricky. We need to find a way to resynchronize it back to the main thread. A Test Expectation can usually help us.
XCTestCase provides methods that wait for one or more test expectations to be fulfilled. If a test waits on an XCTestExpectation, the “wait” method stops. It continues as soon as fulfill is called on the expectation. If the expectation isn’t fulfilled before a given timeout period, the test fails.
To show an XCTestCaseExpectation example, we need a way for the test code to provide a closure to the production code. A natural point to do this is on the didset observer for the results property. It’s where we currently print the results as a substitute for the real work an app might do. Let’s move that work into a new handleResults closure property:
» | var handleResults: ([SearchResult]) -> Void = { print($0) } |
| |
| private(set) var results: [SearchResult] = [] { |
| didSet { |
» | handleResults(results) |
| } |
| } |
Run the app, tap the button, and check the console to confirm that the parsed results are still printed.
Now we have a place for the test to provide its own closure. Before the test calls the data task completion handler, create an XCTestCaseExpectation using the expectation(description:) method. Provide a closure that calls fulfill on the expectation. After calling the data task completion handler, add a wait(timeout:):
» | let handleResultsCalled = expectation(description: "handleResults called") |
» | sut.handleResults = { _ in |
» | handleResultsCalled.fulfill() |
» | } |
| |
| spyURLSession.dataTaskArgsCompletionHandler.first?( |
| jsonData(), response(statusCode: 200), nil |
| ) |
| |
» | waitForExpectations(timeout: 0.01) |
Run tests again. This time, the test passes! (If the test doesn’t pass, check the console output before the assertion failure message. The showError(_:) method prints to the console in addition to showing an alert, so it may give you a clue.)
![]() |
Many examples of waiting for an XCTestCaseExpectation specify a timeout of 1 second. I’ve even seen 10 seconds. But for unit testing where there is no actual networking, that’s an eternity. Try to use 0.01 seconds (10 milliseconds) or less, as shown in the preceding example. |