Why is XCTest wait for expectation hanging an async background task?

2k views Asked by At

I've been experiencing some failures in tests and I've worked out they appear to be caused by XCTest expectation waits suspending the Task instances. Even when they're on a background thread.

Here's a made up test that's a vastly simplified version of the code in my app (please excuse the prints, that's just me mucking around trying to see the sequencing):

    func testTask() async throws {

        let exp = expectation(description: "")
        print("Queuing")
        Task.detached(priority: .background) {
            let duration = try await ContinuousClock().measure {
                print("  Initialing task sleep")
                try await Task.sleep(for:.seconds(1))
            }
            print("  Fulfilling after \(duration)")
            exp.fulfill()
        }

        print("Waiting")
        wait(for: [exp], timeout: 4.0)
        print("Finished")
    }

Now when I run this test the task executes on a background thread and suspends as expected, however it stays suspended for at least 4 seconds and doesn't fulfil until after the expectation has timed out.

Everything I've read so far suggests that you should be able to use expectations with Tasks but so far it's not worked for me.

Am I missing something, or will I have to write some await code to act like an expectation instead?

Notes: This test is a vastly simplified version of a situation in my app. So whilst it may make no sense as a standalone test, it's an accurate representation of what I'm testing. There is also the notion of a traditional completion in it because the real code triggers background tasks which then notify other code of when they finish.

2

There are 2 answers

2
Jon Reid On BEST ANSWER

If you can remove the async from the test declaration, then this works where wait(for:) doesn't:

waitForExpectations(timeout: 4.0)

It's also possible to await the expectations in an async test:

await waitForExpectations(timeout: 4.0)

I have no explanation for why this works, and wait(for: [exp], timeout: 4.0) does not.

0
Michal Šrůtek On

Apple has admitted a problem with wait function in combination with async/await - as per Xcode 14.3 Release Notes.

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)

So, if you're using Xcode 14.3 (or higher), you should use async version of fulfillment(of:timeout:enforceOrder:)