Testing Code That Uses Randomness in Rust Reliably
Make code that uses random numbers testable: inject the generator, seed it, test properties instead of values, and keep production randomness unpredictable.

Random numbers make tests either flaky or useless: a test that passes 999 times out of 1000 fails a build at random, and a test that ignores the random part tests nothing. The cure is to make the generator an input. Everything compiles on Rust 1.90 with edition 2024 and rand 0.9.
The hidden-input version
use rand::Rng;
pub fn roll_dice() -> u8 {
rand::rng().random_range(1..=6)
}A test can only assert 1..=6, and that tells you little. Anything built on this (a game turn, a retry delay with jitter) inherits the problem.
Inject the generator
use rand::Rng;
pub fn roll_dice(rng: &mut impl Rng) -> u8 {
rng.random_range(1..=6)
}
pub fn retry_delay_ms(attempt: u32, rng: &mut impl Rng) -> u64 {
let base = 100u64 * 2u64.pow(attempt.min(6));
let jitter = rng.random_range(0..=base / 2);
base + jitter
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{rngs::StdRng, SeedableRng};
#[test]
fn same_seed_same_rolls() {
let mut a = StdRng::seed_from_u64(7);
let mut b = StdRng::seed_from_u64(7);
let xs: Vec<u8> = (0..10).map(|_| roll_dice(&mut a)).collect();
let ys: Vec<u8> = (0..10).map(|_| roll_dice(&mut b)).collect();
assert_eq!(xs, ys);
}
#[test]
fn delay_stays_in_bounds() {
let mut rng = StdRng::seed_from_u64(1);
for attempt in 0..10 {
let d = retry_delay_ms(attempt, &mut rng);
let base = 100u64 * 2u64.pow(attempt.min(6));
assert!(d >= base && d <= base + base / 2);
}
}
}The first test shows that the seed controls the stream. The second checks the property that matters, bounds, across many draws. Production code calls roll_dice(&mut rand::rng()), which uses the thread-local generator seeded from the operating system.
Properties beat values
Asserting roll_dice(&mut rng) == 4 for seed 7 passes today and may fail after a rand upgrade that changes the algorithm. A property such as "all values in 1..=6, and over 600 rolls every face appears" survives upgrades. For shuffles and samples:
use rand::seq::SliceRandom;
use rand::{rngs::StdRng, SeedableRng};
#[test]
fn shuffle_is_a_permutation() {
let mut rng = StdRng::seed_from_u64(99);
let mut v: Vec<u32> = (0..50).collect();
v.shuffle(&mut rng);
let mut sorted = v.clone();
sorted.sort();
assert_eq!(sorted, (0..50).collect::<Vec<_>>());
assert_ne!(v, sorted);
}The last assertion could in theory fail with probability 1/50!, which is not a practical concern for a fixed seed: it either passes now and always, or you pick another seed.
Let a library search many seeds
proptest runs a property over many generated inputs and shrinks failures, so you get breadth without choosing seeds by hand (property testing). When it finds a failure it prints the seed to replay.
What not to do
- Seeding the global generator in production for reproducibility; security tokens, session ids and nonces must come from the OS generator (secrets handling).
- Sharing one global generator among tests; use one per test.
- Using the system time as the seed in a test. It makes failures unreproducible.
Time is the other hidden input and has its own treatment in injected clocks.


