Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ambient-time

Level: warn · Article: Tests

Some things are genuinely hard to test: the clock, random number generators, external HTTP calls. The instinct is to mock them. The right move is to make them injectable.

What it checks

A call to SystemTime::now(), Instant::now(), Utc::now(), Local::now(), OffsetDateTime::now_utc() or the like anywhere but main. The wall clock is a dependency, and this one is hidden from every signature that uses it.

Don’t

#![allow(unused)]
fn main() {
impl Session {
    fn create(user_id: UserId) -> Session {
        let now = Utc::now();
        Session {
            created_at: now,
            expires_at: now + Duration::hours(1),
            user_id,
        }
    }
}
}

The test for “expires one hour after creation” has to compute the current hour, or sleep, or give up.

Do

#![allow(unused)]
fn main() {
trait Clock: Send + Sync {
    fn now(&self) -> DateTime<Utc>;
}

struct Sessions<C: Clock> {
    clock: C,
}

impl<C: Clock> Sessions<C> {
    fn create(&self, user_id: UserId) -> Session {
        let now = self.clock.now();
        Session {
            created_at: now,
            expires_at: now + Duration::hours(1),
            user_id,
        }
    }
}

#[test]
fn expires_one_hour_after_creation() {
    let clock = FixedClock(Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap());
    let session = Sessions { clock }.create(UserId::new());
    assert_eq!(session.expires_at.hour(), 13);
}
}

The bar for injection: would it be useful outside tests? A Clock is. You freeze time in staging, simulate midnight rollover in a demo, replay a historical scenario.

Silence it

#![allow(unused)]
fn main() {
let started = Instant::now(); // rabot: allow(ambient-time) request timing for the log line; nothing branches on it
}