Deterministic dependencies and concurrency tests
A deterministic test controls the inputs that decide behavior. Wall-clock time, random identifiers, task scheduling, global state, and live network responses are inputs even when they are not ordinary function parameters.
Inject decisions, not delays
struct TestClock { private(set) var now: Duration = .zero mutating func advance(by duration: Duration) { now += duration }}
struct IDSequence { private var values: [UUID]
mutating func next() -> UUID { precondition(!values.isEmpty, "Test requested an unexpected identifier") return values.removeFirst() }}Production can use Swift’s clock APIs and UUID.init; tests supply values deliberately. A production interface should preserve the behavior needed for timeouts and suspension rather than exposing only a timestamp if the use case sleeps.
Observe protocol events
For an async sequence, the test controls when elements arrive and when the sequence finishes. For cancellation, start the operation, wait until a recording dependency reports entry, cancel the parent task, then assert cleanup and no later write. This creates happens-before relationships without guessing how long a task needs.
Test actors through behavior
Do not assert executor threads or scheduling order that Swift does not promise. Send concurrent operations, await completion, and assert the actor’s observable invariant. If exact event order is a product requirement, encode ordering in the actor’s state machine and test that policy.
Unsupported fake operations should fail loudly. A fake that silently ignores invalid states can make tests greener than production.
Series navigation
- Previous: Part 73: XCTest, XCUITest, test plans, and framework coexistence
- Next: Part 75: SwiftUI, UIKit, navigation, accessibility, and UI behavior tests
- Series index: Zero to iOS Hero
References
- Swift concurrency defines task, actor, cancellation, and sendability semantics.
- Clock describes Swift’s time and suspension abstraction.