Migrating to Swift Testing from XCTestExpectation with timeouts

Am I missing something or is Swift Testing missing a timeout functionality in the confirmation() API?


Details:

In asynchronous unit tests, I often have this pattern where I create an XCTestExpectation, run some code that either takes a callback closure (or code that runs in a Task) and then await the fulfillment of the expectation. For example:

func testSomething() async {
    let expectation = self.expectation()
    doSomething(onCompletion: {
        expectation.fulfill()
    })
    await self.fulfillment(of: [expectation], timeout: 1.0)
}

(Non-async test methods use self.wait(for: [expectation], timeout: 1.0) instead, which is equivalent for the purposes of this test.)

The point is: If everything runs well, the unit test completes quickly. But if my doSomething(onCompletion:) method has an issue and never calls the completion handler, the timeout: 1.0 takes care that the unit test fails. I don’t see an equivalent in Swift Testing for this pattern.

AFAICT, this would be the equivalent code in Swift Testing if there were no timeout:

@Test
func testSomething() async {
    await confirmation { completionHandlerCalled in
        doSomething(onCompletion: {
            completionHandlerCalled()
        })
    }
}

The issue is that because there is no timeout here, if my doSomething(onCompletion:) doesn’t call the completion handler, the unit test never finishes and does not fail. This is especially problematic in CI setups where the whole thing just hangs there, possibly for hours, until some higher-level timeout mechanism kicks in.

I am aware of the migration guide from Apple. The section about migrating from XCTestExpectation to confirmation() conveniently glosses over the fact that the former supported a timeout and the latter seemingly does not.

I see that there is a TimeLimitTrait for the @Test macro, but that is not what I’m looking for. First, that timeout can only be specified in whole minutes (:face_with_raised_eyebrow:). Second, this trait applies to the whole test, but sometimes I would like to test multiple expectations after each other and await each of them separately with their own timeouts.

So am I missing something here or is Swift Testing maybe waiting for some more fundamental timeout mechanism for calling async code?

I feel stupid replying to my own question, but I just found that this is discussed in the issues section of the Swift Testing GitHub repo already. I somehow didn’t realize that this exists, so sorry for the noise.

All good! See also Add an overload of `confirmation()` with a timeout by grynspan · Pull Request #789 · swiftlang/swift-testing · GitHub

1 Like