For what it's worth, those posts generally predate Swift Testing moving into the toolchain and involve building it and swift-syntax from source code (in addition to your test code.)
I just published a small library with a Swift Testing replacement for XCTestExpectation. Hopefully one day it’ll be Sherlocked, but for now this gets the job done.
here is my submission for this problem which I'm now using:
Thanks to this thread and the great learnings I got from it, I was able to finally use Swift Testing with asynchronous confirmations.
Below you can find the solution I came with, using the OP code as an example, marking with numbers my additions:
@Test
func example() async {
let (stream, continuation) = AsyncStream<Void>.makeStream() /// 1
/// Create `sut` here ...
await confirmation { confirmed in
env.analytics.trackEventMock.whenCalled { _ in
confirmed()
continuation.yield() /// 2
}
/// this should be awaited now as we're not on the main actor
await sut.start()
var iterator = stream.makeAsyncIterator() /// 3
await iterator.next() /// 4
#expect(env.analytics.trackErrorMock.calledOnce)
}
}
I prefer this approach as opposed to having the other initialiser for AsyncStream
as this makes the code more readable.
Also, I prefer this approach as opposed to Task.sleep
as this doesn't have to just "waste" a few arbitrary seconds just waiting.
Hello,
I refactored your solution into a utility function:
/// Waits for a confirmation up to the provided `timeout` duration.
/// - Parameters:
/// - message: An optional comment to apply to any issues generated by this function.
/// - expectedCount: The number of times the expected event should occur when body is invoked. The default value of this argument is 1, indicating that the event should occur exactly once. Pass 0 if the event should never occur when body is invoked.
/// - timeout: The maximum amount of time to wait for the confirmation to happen.
/// - isolation: The actor to which body is isolated, if any.
/// - sourceLocation: The source location to which any recorded issues should be attributed.
/// - body: The function to invoke.
/// - Throws: Whatever is thrown by body.
/// - Returns: Whatever is returned by body.
func waitingConfirmation<R>(
_ message: Comment? = nil,
expectedCount: Int = 1,
timeout: Duration = .seconds(1),
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation = #_sourceLocation,
_ body: (@Sendable @escaping () -> Void) async throws -> sending R
) async rethrows -> R {
try await confirmation(
message,
expectedCount: expectedCount,
isolation: isolation,
sourceLocation: sourceLocation
) { confirmation -> R in
let (stream, continuation) = AsyncStream<Void>.makeStream()
let result = try await body({
confirmation.confirm()
continuation.yield()
})
try await withThrowingTaskGroup { group in
group.addTask {
var iterator = stream.makeAsyncIterator()
await iterator.next()
}
group.addTask {
try await Task.sleep(for: timeout)
continuation.yield()
throw CancellationError()
}
try await group.next()
group.cancelAll()
}
return result
}
}
Shouldn't the expectedCount argument be the expectedCount parameter instead of 1
thank you, fixed!
No, I should be the one thanking you, I think you have the solution to my most recent issue before testing asynchronous closures
Nice one, @TizianoCoroneo !