Unit testing async functions in Xcode
Trying to test async methods can be a challenge — normally the testing method will complete execution before your async methods can run thereby skipping all of your assertions. The solution is to create an XCTestExpectation
and wait()
for its fulfillment:
func testAsyncMethods() {
let expectation1 = XCTestExpectation(description: "Something completed.")
asyncMethod(onCompletion: { (result) in
XCTAssertTrue(result) // You can assert whatever is necessary here!
expectation1.fulfill()
})
wait(for: [expectation1], timeout: 1.0)
}
Probably goes without saying, but the timeout
parameter is there to limit how long the wait()
method will pause to allow your expectation to fulfill. If it does not do so within the given timeout the test will fail.
Counting fulfillment
When using XCTestExpectation
you can actually fulfill an expectation several times - this won’t cause any errors by default (this changed by the way — the older XCTestCase
APIs used to fail on over-fulfillment). But in certain cases you may actually want to specify exactly how many times a fulfillment should occur.
A good example of this would be responding to events — you would clearly want your listener method to be called exactly the number of times the event is triggered. To test such a scenario you can use the expectedFulfillmentCount
and assertForOverFulfill
properties of an expectation:
let expectation2 = XCTestExpectation(description: "Expected exactly 5 times.")
expectation2.expectedFulfillmentCount = 5
expectation2.assertForOverFulfill = true
// ...
wait(for: [expectation2], timeout: 1.0)
As you probably figured out expectation2
will complete successfully only if its fulfill()
method is called exactly 5 times. Any more or less will result in a failed test.
Ensuring the correct order
It may be the case that you want to make sure your asynchronous expectations are fulfilled in the expected order. In this case make use of the enforceOrder
parameter:
wait(for: [expectation1, expectation2], timeout: 1.0, enforceOrder: true)
Final note
Whether you are adding tests retroactively (yeah, yeah, it happens) or adhering to strict TDD, remember that especially with async tests you should always create a failing test first. If you skip this step, you may end up falsely assuming your assertions all pass, even though they never actually run. From copy&paste mistakes to accidentally forgetting to add an expectation to the wait()
array it is quite easy to just unknowingly skip a set of assertions.
Simply making sure they fail when they should will let you know the checks are running properly — only then proceed to resolve the failure either by fixing the test or implementing the underlying code.