Testing concurrent code without sleeps

Testing Senior Swift 5.9+ iOS 17+ Reviewed By Alok Singh

Pick the fix

A view model refreshes every 30 seconds. How do you test it without waiting?

Options
Hint 1 of 1

What does the code depend on?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Inject a Clock, use a test clock in tests, advance it and await the refresh

Correct. Time becomes a dependency the test controls.

Why not the others?
  • B Call Task.sleep(for: .seconds(31)) in the test. That's a 31-second test, and still flaky.
  • C Run the test 10 times and accept it if most pass. Flaky tests hide real bugs.
  • D Turn off parallel testing. It doesn't remove the dependency on real time.

30-second answer

Make time and scheduling dependencies. Inject a Clock so tests can use a controllable clock, await the real work or its returned Task, and use confirmations or AsyncStream continuations to signal events. Run tests many times and with Thread Sanitizer to catch races, rather than adding longer sleeps.

Practise saying it · 45 s. No recording, just a timer

Explain how you test concurrent code.

Say your answer out loud, then check which of these points you covered:

  • Inject clocks
  • Await real work
  • Signal events
  • Sanitizers

Deep dive

Return the Task from fire-and-forget methods so tests can await it.

Expose an AsyncStream of events for tests to iterate.

Thread Sanitizer finds data races at runtime.

Swift 6 strict checking removes many races at compile time.

Injected clock Type-checked · Swift 6.4
@MainActor @Observable
final class FeedModel {
    private let clock: any Clock<Duration>
    init(clock: any Clock<Duration> = ContinuousClock()) { self.clock = clock }

    func autoRefresh() async {
        while !Task.isCancelled {
            await refresh()
            try? await clock.sleep(for: .seconds(30))
        }
    }
    func refresh() async { /* load */ }
}

Common mistakes

  • Increasing sleep durations until CI passes.
  • Testing debounce with real time.

Senior take & follow-ups

Seniors design production code with seams that make concurrency testable.

Follow-up questions

Think of your answer first, then open the hint.

  1. How do you test a 300 ms debounce instantly?

    Show hint

    An injected test clock you advance.

  2. How do you test a fire-and-forget method?

    Show hint

    Return or expose its Task and await it.

Suggest a fix if something here is wrong or out of date.