Testing Time Dependent Code in Rust With Injected Clocks
Make code that depends on the clock testable: pass the time in, define a Clock trait or closure, use a fake clock that you advance, and pause time in Tokio tests.

Anything that reads the clock is nondeterministic, and tests of nondeterministic code are either slow or flaky. The fix is to treat the current time as an input. Everything compiles on Rust 1.90 with edition 2024, chrono 0.4 and tokio 1.
Level 1: pass the time as an argument
use chrono::{DateTime, Duration, Utc};
pub struct Session {
pub created: DateTime<Utc>,
}
impl Session {
pub fn is_expired(&self, now: DateTime<Utc>) -> bool {
now - self.created > Duration::hours(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn expires_after_an_hour() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
let s = Session { created: start };
assert!(!s.is_expired(start + Duration::minutes(59)));
assert!(!s.is_expired(start + Duration::hours(1)));
assert!(s.is_expired(start + Duration::hours(1) + Duration::seconds(1)));
}
}The test sets the exact boundary and runs in microseconds. The caller at the top of the app passes Utc::now(). This is the best option when time is read in a few places: it needs no trait, no mock and no lifetime plumbing.
Level 2: a Clock trait
When many components read the time, pass a clock object:
use chrono::{DateTime, Duration, Utc};
use std::sync::{Arc, Mutex};
pub trait Clock: Send + Sync {
fn now(&self) -> DateTime<Utc>;
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> DateTime<Utc> { Utc::now() }
}
#[derive(Clone)]
pub struct FakeClock(Arc<Mutex<DateTime<Utc>>>);
impl FakeClock {
pub fn at(t: DateTime<Utc>) -> Self { FakeClock(Arc::new(Mutex::new(t))) }
pub fn advance(&self, d: Duration) { *self.0.lock().unwrap() += d; }
}
impl Clock for FakeClock {
fn now(&self) -> DateTime<Utc> { *self.0.lock().unwrap() }
}
pub struct TokenStore<C: Clock> {
clock: C,
issued: Vec<DateTime<Utc>>,
}
impl<C: Clock> TokenStore<C> {
pub fn new(clock: C) -> Self { Self { clock, issued: vec![] } }
pub fn issue(&mut self) { self.issued.push(self.clock.now()); }
pub fn live(&self) -> usize {
let cutoff = self.clock.now() - Duration::minutes(10);
self.issued.iter().filter(|t| **t > cutoff).count()
}
}A test builds a FakeClock, issues tokens, calls clock.advance(Duration::minutes(11)) and checks live(). The generic parameter has no run-time cost; use Arc<dyn Clock> if the type parameter spreads too far (static versus dynamic dispatch). Crates such as mock_instant or time's test helpers package the same idea.
Tokio timers
use std::time::Duration;
#[tokio::test(start_paused = true)]
async fn retries_after_a_delay() {
let start = tokio::time::Instant::now();
tokio::time::sleep(Duration::from_secs(30)).await;
assert!(start.elapsed() >= Duration::from_secs(30));
}With start_paused, the clock does not move on its own. When every task is waiting on a timer, the runtime advances virtual time to the next deadline immediately, so a 30 second sleep returns at once. It needs Tokio's test-util feature. Use tokio::time::advance(d) for finer control.
Pitfalls
Instantis monotonic and for durations;SystemTimeandDateTimeare for wall time. Do not mix them.- Time zones and daylight saving: test with fixed UTC instants and a few zones, not with the machine's local zone.
- Reading the clock twice in one operation can give two different values; read once and pass it down.
- Randomness is the other hidden input; see randomness.


