I've encountered a problem with awaiting expectations in XCTestCase when testing async code. I want to test code that is supposed to execute operations in background and notify me about the result.
The problem is that once background operation finishes nothing happens. Instead my expectation times out after the specified time. Below is a simplified model and test case that simulates my code. Running it displays only "before doSomethingAsync" and "in doSomethingAsync" messages in console. It looks to me like execution is not returning to main actor while waitForExpectations is running.
I noticed that there is a note in documentation that says âClients should not manipulate the run loop while using this API.â. Perhaps this means that something like Iâm trying to do is not supported. If that is the case is there an alternative approach?
Unfortunately that wonât work. doSomething launches a Task that wonât complete until later. There is an await in test code only because it runs outside of MainActor. I could annotate whole test with @MainActor like below but that wonât resolve the issue.
final class AsyncTestTests: XCTestCase {
@MainActor
func testDoSomething() async throws {
let exp = expectation(description: "task finished")
let viewModel = ViewModel(didRun: {
exp.fulfill()
})
viewModel.doSomething()
waitForExpectations(timeout: 5)
}
}
Fixed: XCTestCase.wait(for:timeout:enforceOrder:) and related methods are now marked unavailable in concurrent Swift functions because they can cause a test to deadlock. Instead, you can use the new concurrency-safe XCTestCase.fulfillment(of:timeout:enforceOrder:) method. (91453026)
If youâre still curious about the root cause, I think the issue with waitForExpectations in async code is that calling it blocks the main thread/queue. Since the async tasks need to run on the main queue too, they wait for the queue to clear up, but the queue never clears up because the XCTestExpectation blocking it is waiting for the async tasks â creating a deadlock.
In general you should avoid blocking any execution context thatâs running async code (whether thatâs the main thread or a global async executor) because that violates the forward progress invariant. Thereâs a great video on this from WWDC 2021: see Swift concurrency: Behind the scenes - WWDC21 - Videos - Apple Developer.