Integration Testing

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