tezvyn:

Testing Async Code with XCTestExpectation

AI-drafted, machine-checkedSource: developer.apple.comintermediate
Testing Async Code with XCTestExpectation

XCTestExpectation is a stopwatch for your tests, pausing them to wait for asynchronous work like a network call to finish. Use it for testing code with completion handlers. The footgun is forgetting to `fulfill()` the expectation, causing a timeout failure.

WHY IT EXISTS: Standard unit tests execute synchronously, finishing as soon as the last line of the function is run. This model breaks when testing asynchronous operations like network calls, as the test would finish and pass before the operation even completes. XCTestExpectation was created to bridge this gap, allowing a test to pause and wait for a result.

THE MENTAL MODEL: Think of an XCTestExpectation as a promise you make to the test runner. You tell it, "I expect this specific event to happen." Then you start your asynchronous work. When that work finishes and calls its completion handler, you signal fulfill() on the expectation. The test runner waits a specified amount of time for that signal. If it gets the signal, the test proceeds. If the timeout is hit first, the test fails.

HOW IT WORKS: The process involves three key steps. First, you create an expectation with a clear description, like let expectation = self.expectation(description: "Fetch user profile"). Second, you call your asynchronous method. Inside its completion block, after you've run any assertions, you must call expectation.fulfill(). Third, the final line of your test method must be waitForExpectations(timeout: 5.0). This call blocks the test from finishing until either the expectation is fulfilled or the 5-second timeout expires.

WHEN TO USE IT: Use expectations for testing code that signals completion asynchronously. This is common with older callback-based APIs using completion handlers, the delegate pattern, or NotificationCenter. It's the standard tool for testing asynchronous code that doesn't use Swift's modern concurrency features.

WHEN NOT TO USE IT: Avoid expectations for purely synchronous code, as it adds needless complexity. For modern Swift code using async/await, you can often write simpler, more readable tests by marking the test function as async and using await on your asynchronous calls. This lets the test suspend and resume naturally without manual expectation management.

ONE CANONICAL EXAMPLE: Imagine testing a NetworkClient with a function fetch(url:completion:). Your test would create an XCTestExpectation with the description "API fetch completes". It then calls client.fetch(...). Inside the completion handler, you'd assert that the result is successful and then call expectation.fulfill(). Crucially, you must also call fulfill() in the error case to prevent a timeout. The test concludes with waitForExpectations(timeout: 1.0).

Read the original → developer.apple.com

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.