Testing callbacks with confirmations in Swift Testing
Learn how to verify that callback-based events occur the expected number of times.
26 Oct 2026 · 4 min read
Testing an asynchronous function is often as simple as awaiting its result and checking the returned value. Some APIs, however, report events through a callback while an asynchronous operation is running.
Swift Testing provides confirmations for verifying that these events occur and that a callback is invoked the expected number of times.

Starting with a progress callback
Suppose a file importer calls a progress handler after importing each file:
struct FileImporter {func importFiles(_ files: [String],onProgress: @Sendable (String) -> Void) async {for file in files {await importFile(named: file)onProgress(file)}}}
Awaiting importFiles tells us when the complete operation finishes, but its return value doesn't tell us whether onProgress was called for every file.
Confirming callback occurrences
The confirmation function gives us a value that we call whenever the expected event occurs. By passing the number of files as expectedCount, we can verify that the importer reports one progress event per file:
import Testing@testable import ExampleAppstruct FileImporterTests {@Test func reportsProgressForEveryImportedFile() async {let files = ["article.md", "cover.png", "metadata.json"]let importer = FileImporter()await confirmation(expectedCount: files.count) { progressReported inawait importer.importFiles(files) { importedFile in#expect(files.contains(importedFile))progressReported()}}}}
Every call to progressReported() confirms one occurrence. The test passes only if the confirmation is called exactly three times. Calling it fewer or more times records an issue, while the expectation inside the callback also verifies that each reported file belongs to the import.
Without an expectedCount, a confirmation expects exactly one occurrence. Providing the count makes the relationship between the input files and the expected progress events explicit.
Confirming that an event doesn't occur
An expected count of zero lets us verify that an event doesn't happen. For example, importing an empty collection shouldn't report any progress:
@Test func doesNotReportProgressForEmptyImport() async {let importer = FileImporter()await confirmation(expectedCount: 0) { progressReported inawait importer.importFiles([]) { _ inprogressReported()}}}
The test passes when the closure completes without calling progressReported(). Any progress event makes it fail.
How confirmations observe events
A confirmation only observes events while its trailing closure is running. It doesn't keep the closure alive until a callback eventually occurs. Once the closure returns, Swift Testing checks how many times the confirmation was called.
In our examples, importFiles is awaited inside the confirmation closure. It doesn't return until the import and its progress callbacks have finished, so all expected events occur before Swift Testing checks their count.
This is different from an API that starts work and returns immediately:
imageLoader.loadImage { result in// Called sometime later}
The confirmation closure could return before this callback runs. If the callback produces a single result, it is usually better to expose it as an asynchronous function using a continuation:
func loadImage() async throws -> Image {try await withCheckedThrowingContinuation { continuation inimageLoader.loadImage { result incontinuation.resume(with: result)}}}
The continuation suspends loadImage() until the callback provides its result. A test can then await the image and inspect it directly.
Conclusion
Confirmations let us verify how often callback-based events occur while an asynchronous operation is running. They are a good fit when a callback represents an event that may occur zero, once, or several times during an operation.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



