Unit testing async functions in Xcode

Aron Budinszky
hoursofoperation
Published in
2 min readSep 18, 2022

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.

Waiting is the key. :) Photo by Denys Nevozhai on Unsplash

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.

--

--