Skip to content

Async Testing and Waiting

Use this file when tests involve async/await functions, completion handlers, streams/events, or timing-related flakiness.

  • Use async test functions and await naturally.
  • Keep async test code close to production async patterns.
  • Prefer structured concurrency patterns over ad-hoc synchronization.
  • Prefer confirmations for async event-style tests that are not naturally awaitable.
import Testing
struct APIClient {
func fetchName() async throws -> String { "Antoine" }
}
@Test func fetchNameReturnsValue() async throws {
let client = APIClient()
let value = try await client.fetchName()
#expect(value == "Antoine")
}
  • For completion-handler APIs without async overloads, bridge with:
    • withCheckedContinuation
    • withCheckedThrowingContinuation
  • Keep continuation wrappers minimal and test-focused.
import Testing
func legacyLoad(_ completion: @escaping (Result<Int, Error>) -> Void) {
completion(.success(42))
}
@Test func legacyAPI() async throws {
let value = try await withCheckedThrowingContinuation { continuation in
legacyLoad { result in
continuation.resume(with: result)
}
}
#expect(value == 42)
}
  • Use confirmations when validating event delivery/count semantics that do not map cleanly to direct await.
  • Set expected counts explicitly:
    • exact count for strict validation
    • lower-bounded range for at-least semantics
  • Keep confirmation scope small and ensure confirmations happen before the confirmation block returns.
import Testing
@Test func eventIsPublishedTwice() async {
await confirmation("Publishes two events", expectedCount: 2) { confirm in
confirm()
confirm()
}
}
  • Avoid unsafe mutable shared counters from callback closures in strict concurrency mode.
  • Use isolation-safe patterns (actor state, AsyncSequence wrappers, or thread-safe containers).
  • Verify callback counts and ordering explicitly when behavior depends on it.
import Testing
actor EventCounter {
private(set) var count = 0
func increment() { count += 1 }
}
@Test func countEventsSafely() async {
let counter = EventCounter()
await counter.increment()
await counter.increment()
#expect(await counter.count == 2)
}
  • Do not return from test before async callback work completes.
  • Avoid sleeping/time-based waits as primary synchronization.
  • Replace brittle waiting with awaitable conditions and deterministic synchronization points.
// Avoid this pattern:
// try await Task.sleep(nanoseconds: 500_000_000)
// #expect(flag == true)
  • Isolate tests to a global actor (e.g. @MainActor) only when behavior truly requires it.
  • Keep non-UI tests off main actor to preserve realistic concurrency behavior.
import Testing
@MainActor
@Test func uiModelMutation() {
#expect(true)
}