Recipe
Test a timeout or a delay without waiting for it
Use when: async code waits, with a timeout, a sleep or a pause between retries, and a test must prove how long it waits without taking that long
Goal: a #[tokio::test(start_paused = true)] test that runs in no real time and checks each duration to the millisecond
Taught in Testing async code
Quick recipe
-
1
Mark the test
#[tokio::test(start_paused = true)]. Turn on Tokio'stest-utilfeature in the dev-dependencies.Example
tokio = { version = "1", features = ["full", "test-util"] }under[dev-dependencies], then#[tokio::test(start_paused = true)]aboveasync fn a_quick_quote_arrives().Why: with the clock paused, Tokio jumps to the next timer whenever every task is waiting.
-
2
Check that each wait in the code goes through
tokio::time, such assleeportimeout.Example
quote_or_give_upwaits withsleep(answer_after)insidetimeout(limit, ...). Both are Tokio timers, so even an hour-longsleepcosts no real time.- the wait is
tokio::time::sleeportimeout— the paused clock skips it. - the wait is
std::thread::sleep, a long loop or aspawn_blockingjob — it takes real time, so replace it with a fake that callstokio::time::sleep.
Why: the paused clock only controls Tokio's own timers, and it stops skipping while blocking work runs.
- the wait is
-
3
Write
let start = tokio::time::Instant::now();before the call, and readstart.elapsed()after it.Example
With
answer_afterset to 3600 seconds andlimitto 1 second,start.elapsed()comes out asDuration::from_secs(1).Why:
std::time::Instantreads the real clock, which barely moves during a paused test. -
4
Decide whether the test checks only the end, or a moment in between.
Example
The reminder test spawns
remind_afterwith 600 seconds and yields. It advances 599 seconds and checks that nothing has arrived. Then it advances 1 more second and receives the message.- only the end —
.awaitthe call, and auto-advance jumps to each timer. - a moment between timers — spawn the task, call
tokio::task::yield_now().await, thenadvance(...)to that moment.
Why: a spawned task doesn't run until the test yields, so without
yield_nowits timer starts late. - only the end —
-
5
Compare
start.elapsed()with the sum of the waits, usingassert_eq!. Wrap any wait that might never end in atimeout, so a bug fails the test instead of hanging it.Example
assert_eq!(start.elapsed(), Duration::from_secs(600));in the reminder test. On the paused clock, 600 seconds means 600, not 600.002.Why: pretend time never runs late, so a wrong wait shows up as a different number, not as a flaky test.
Common mistakes
- Measuring with
std::time::Instant. In a paused test it shows almost no time passing. - Calling
advancestraight aftertokio::spawn, before the task has started its timer. - Leaving real blocking work in a timing test. The clock stops skipping while it runs.
- Putting a plain
#[test]on anasync fn. It doesn't compile.