Deterministic Tests
A deterministic test always produces the exact same pass or fail result for a given version of the code, regardless of the time of day, machine CPU load, hash table randomization, or thread scheduling order. Eliminating non-determinism prevents flaky tests—tests that intermittently fail in CI without any relevant code change.
Sources of Flakiness in Tests Deterministic Replacement
----------------------------- -------------------------
1. Wall-clock `SystemTime::now()` ---> Inject a `Clock` trait or `Instant`/`SystemTime` parameter
2. `thread::sleep(50ms)` timing ---> Synchronize via channels, `Condvar`, or `Barrier`
3. Unordered `HashMap` iteration ---> Sort output before asserting, or use `BTreeMap`
4. Unseeded random generators ---> Seed PRNG with a fixed `u64` seed (and log the seed!)
Controlling Time Without SystemTime::now() or sleep()
Code that calls SystemTime::now() directly inside business logic is difficult to test around leap seconds, expiration boundaries, or multi-hour TTLs. Passing the current timestamp as a parameter—or abstracting time behind a Clock trait—makes expiration tests instantaneous and 100% deterministic:
use std::time::{Duration, UNIX_EPOCH, SystemTime};
pub trait Clock {
fn now(&self) -> SystemTime;
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> SystemTime {
SystemTime::now()
}
}
pub struct FakeClock {
pub current: SystemTime,
}
impl FakeClock {
pub fn new(epoch_secs: u64) -> Self {
Self {
current: UNIX_EPOCH + Duration::from_secs(epoch_secs),
}
}
pub fn advance(&mut self, dur: Duration) {
self.current += dur;
}
}
impl Clock for FakeClock {
fn now(&self) -> SystemTime {
self.current
}
}
pub struct Session {
expires_at: SystemTime,
}
impl Session {
pub fn create(clock: &impl Clock, ttl: Duration) -> Self {
Self {
expires_at: clock.now() + ttl,
}
}
pub fn is_valid(&self, clock: &impl Clock) -> bool {
clock.now() < self.expires_at
}
}
fn main() {
let mut clock = FakeClock::new(1_700_000_000);
let session = Session::create(&clock, Duration::from_secs(3600));
// At T + 3599s: still valid
clock.advance(Duration::from_secs(3599));
assert!(session.is_valid(&clock));
// At T + 3600s: expired! Tested in 0ms without sleeping.
clock.advance(Duration::from_secs(1));
assert!(!session.is_valid(&clock));
println!("Deterministic session expiry verified without thread::sleep");
}
Deterministic session expiry verified without thread::sleep
Avoiding Order-Dependent HashMap / HashSet Assertions
Rust's standard HashMap and HashSet randomize their RandomState SipHash keys on every process invocation to prevent HashDoS attacks. Consequently, iterating over a HashMap yields elements in a different order on every test run.
Never assert on the raw string or Vec produced by iterating a HashMap without sorting first:
use std::collections::HashMap;
fn format_query_params(params: &HashMap<&str, &str>) -> String {
let mut pairs: Vec<String> = params
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
// Sort keys deterministically before joining!
pairs.sort();
pairs.join("&")
}
fn main() {
let map = HashMap::from([("page", "2"), ("limit", "50"), ("sort", "asc")]);
// Guaranteed to be identical on every run and platform
assert_eq!(format_query_params(&map), "limit=50&page=2&sort=asc");
println!("Deterministic query string: {}", format_query_params(&map));
}
Deterministic query string: limit=50&page=2&sort=asc