Logo for tanaschita.com

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.

Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE

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 ExampleApp
struct FileImporterTests {
@Test func reportsProgressForEveryImportedFile() async {
let files = ["article.md", "cover.png", "metadata.json"]
let importer = FileImporter()
await confirmation(expectedCount: files.count) { progressReported in
await 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 in
await importer.importFiles([]) { _ in
progressReported()
}
}
}

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 in
imageLoader.loadImage { result in
continuation.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.

Sponsorship logo
Preparing for a technical iOS job interview
Preparing for a technical iOS Job Interview with over 300 questions & answers. Covering Swift & Objective-C, SwiftUI & UIKit, Combine, HTTP Networking, iOS File System, Core Data, Concurrency with async/await, Security, Automated Testing, Dependency Management, AI & Machine Learning and more.
LEARN MORE
Sponsorship logo
Architecture & Design Patterns for iOS
This book is a practical guide to essential architectural principles and design patterns for iOS development. It covers strategies for building maintainable apps with Swift and SwiftUI, including dependency injection, navigation, common patterns, and modularization.
LEARN MORE
Sponsorship logo
Become a sponsor of tanaschita.com
By publishing an article on different iOS topics every week, tanaschita.com is constantly growing in the developer community and may provide a great audience for you as a sponsor.
CLICK TO LEARN MORE

Newsletter

Image of a reading marmot
Subscribe

Like to support my work?

Say hi

Related tags

Articles with related topics

async/await

concurrency

testing

swift

How to unit test async/await functions in Swift

Learn how to write unit tests for async/await functions.

16 Dec 2024 · 5 min read

Latest articles and tips

© 2026 tanaschita.com

Privacy policy

Impressum