Reputation: 16931
How do you write a unit test that checks whether an async function doesn't timeout?
I'm trying with regular XCTestExpectation
, but because await
suspends everything, it can't wait for the expectation.
In the code below, I'm checking that loader.perform()
doesn't take more than 1 second to execute.
func testLoaderSuccess() async throws {
let expectation = XCTestExpectation(description: "doesn't timeout")
let result = try await loader.perform()
XCTAssert(result.value == 42)
wait(for: [expectation], timeout: 1) // execution never gets here
expectation.fulfill()
}
Upvotes: 4
Views: 3488
Reputation: 4521
I suggest you the following function based on Rob's answer:
func testAwait(timeout: UInt64, task: @escaping () async -> Void) async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
await task()
}
group.addTask {
try await Task.sleep(nanoseconds: timeout * NSEC_PER_SEC)
XCTFail("Timed out")
}
let _ = try await group.next()
group.cancelAll()
}
}
This is how you can use it:
try await testAwait(timeout: 1) {
let result = try await loader.perform()
XCTAssert(result.value == 42)
}
Upvotes: 0
Reputation: 437792
It might be prudent to cancel the task if it times out:
func testA() async throws {
let expectation = XCTestExpectation(description: "timeout")
let task = Task {
let result = try await loader.perform()
XCTAssertEqual(result, 42)
expectation.fulfill()
}
await fulfillment(of: [expectation], timeout: 1)
task.cancel()
}
If you do not, perform
may continue to run even after testA
finishes in the failure scenario.
The other approach would be to use a task group:
func testB() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
let result = try await self.loader.perform()
XCTAssertEqual(result, 42)
}
group.addTask {
try await Task.sleep(for: .seconds(1))
XCTFail("Timed out")
}
let _ = try await group.next() // wait for the first one
group.cancelAll() // cancel the other one
}
}
Upvotes: 3
Reputation: 16931
The sequence that worked for me both locally and on CI is the following:
func testLoaderSuccess() async throws {
Task {
let result = try await loader.perform()
XCTAssert(result.value == 42)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
Upvotes: 0
Reputation: 7744
You need to structure this in a different way.
You need to create a new Task
. In this Task
execute and await the async code. After awaiting fulfill the expectation.
Your code did not work because the Thread the Test runs on will stop at wait(for:
for the expectation to fulfill, what it never does as the line comes after wait(for:
.
func testLoaderSuccess() throws {
let expectation = XCTestExpectation(description: "doesn't timeout")
Task{
try await Task.sleep(nanoseconds: 500_000_000)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
// Assertions here because only then is assured that
// everything completed
}
Upvotes: 1