How does confirmation count events in Swift Testing, and what can it not do?

22.8.4 · L5 · Interview

Predict first

Which confirmation counts match, and what happens to an event that arrives after the closure returns?

// === Package.swift ===
// swift-tools-version:6.0
import PackageDescription
let package = Package(name: "Lib", targets: [.target(name: "Lib"), .testTarget(name: "LibTests", dependencies: ["Lib"])])
// === Sources/Lib/Lib.swift ===
public func square(_ x: Int) -> Int { x * x }
// === Tests/LibTests/T.swift ===
import Testing
@testable import Lib
final class Bus: @unchecked Sendable {
    private var handlers: [@Sendable (String) -> Void] = []
    func on(_ h: @escaping @Sendable (String) -> Void) { handlers.append(h) }
    func emit(_ s: String) { handlers.forEach { $0(s) } }
}
@Test func exactlyTwo() async {
    let bus = Bus()
    await confirmation("two events", expectedCount: 2) { got in
        bus.on { _ in got() }
        bus.emit("a"); bus.emit("b")
    }
}
@Test func tooFew() async {
    let bus = Bus()
    await confirmation("two events", expectedCount: 2) { got in bus.on { _ in got() }; bus.emit("a") }
}
@Test func range() async {
    let bus = Bus()
    await confirmation("two to three", expectedCount: 2...3) { got in
        bus.on { _ in got() }
        bus.emit("a"); bus.emit("b"); bus.emit("c"); bus.emit("d")
    }
}
@Test func never() async {
    await confirmation("must not fire", expectedCount: 0) { got in got() }
}
@Test func late() async {
    let bus = Bus()
    await confirmation("late", expectedCount: 1) { got in
        bus.on { _ in got() }
        Task { try? await Task.sleep(for: .milliseconds(100)); bus.emit("x") }
    }
}
  • Exactly two passes; too few, too many, a range miss and a count of zero are reported with their counts; the late event counts as 0
  • The late event is waited for and counted
  • Every confirmation passes

Mental model. A confirmation is a turnstile counter for one scene: it is read when the scene ends. People who arrive after the curtain are not counted.

await confirmation("name", expectedCount: n) { confirm in … } hands the closure a function to call each time the event happens, and when the closure returns it checks that the count matches. It accepts an exact number, a range (2...3), and 0 to assert an event never happens (the inverted expectation of XCTest). The check shows the messages: confirmed once but expected twice, four times but expected 2 ... 3, once but expected 0. It replaces XCTestExpectation for “this callback fires N times”, and because it is a scope, you cannot forget to wait for it.

The catch is the same scope: confirmations after the closure returns are too late. In the check, an event emitted from a Task after 100 ms is counted as 0 confirmations, because the closure had already returned. So the closure must await the work that produces the events (an await on the operation, or the end of an AsyncStream), not merely start it. If the callback API is fire-and-forget, wrap it in a continuation, or await a signal that the operation is complete, then check the count. There is no timeout argument to “wait up to a second”, and that is deliberate: waiting for real time is what makes tests flaky.

Counts are checked when the closure returns, so late events are not seen.

Common traps

  • Starting the work in a Task and returning. The count is checked immediately and the event comes later, so the test fails (or, for count 0, wrongly passes).
  • Using expectedCount: 0 as a “wait to see nothing happens” test. It only covers what happened before the closure returned.

They might follow up with

  • How do I wait for a callback API? Bridge it with withCheckedContinuation and await it.
  • What replaces XCTest’s inverted expectation? A confirmation with an expected count of 0.

Related questions

  • 22.8.3 How do you assert thrown errors in Swift Testing, and what do withKnownIssue and Issue.record add?
  • 22.8.5 Swift Testing runs tests in parallel by default. What does .serialized change, and what does it not fix?
  • 22.3.1 How do you test async code: async test functions, expectations, confirmations, actors and the main actor?
  • 22.3.2 How do you test code that depends on time: debounce, timeouts, retries and expiry?

← How do you assert thrown errors in Swift Testing, and what do…   Swift Testing runs tests in parallel by default. What does… →