Overview
Example-based tests check the inputs you thought of; these two techniques generate the
ones you didn't. Property-based testing (proptest) asserts invariants over
structured random inputs and shrinks failures to minimal counterexamples.
Fuzzing (cargo-fuzz driving libFuzzer) throws coverage-guided hostile bytes at a
target and hunts crashes. Rust's culture treats them as two dials on one machine — the
arbitrary crate lets a fuzz target generate the same structured inputs proptest
uses — and the ecosystem's parsing-heavy, unsafe-adjacent codebases lean on both
harder than Java's or Go's do.
Key points
- proptest strategies compose:
any::<u32>(),"[a-z]{1,8}"(regex-shaped strings),prop::collection::vec(elem, 0..100),prop_oneof!, mapped/filtered into domain types;#[derive(Arbitrary)](via proptest-derive or the arbitrary crate) for structs. - Shrinking is the killer feature: a failing 90-element vector comes back as the
3-element core that still fails; failures persist to
proptest-regressions/files — commit them, they're permanent regression tests. - The classic properties: roundtrips (
decode(encode(x)) == x— serde types, parsers), equivalence to a reference implementation, invariant preservation (sorted stays sorted, balances sum to zero), and no-panic on any input. - quickcheck is the smaller ancestor — still around, but proptest's strategies and shrinking won; new code uses proptest.
- cargo-fuzz:
cargo fuzz initscaffoldsfuzz/fuzz_targets/*.rs; each target is afuzz_target!(|data: &[u8]| { … })closure built with libFuzzer + AddressSanitizer (nightly), corpus-managed underfuzz/corpus. AFL via afl.rs is the alternative engine; structure-aware fuzzing comes from taking|input: MyType|witharbitrary. - What fuzzing finds in safe Rust: panics (slice OOB, unwraps, integer overflow
in debug), OOM/DoS via pathological inputs, logic divergence — memory corruption
needs
unsafe, where fuzz + miri + sanitizers is the standard battery. - Division of labour: proptest in the regular suite (bounded cases, runs in CI on every push); fuzzing as long-running background/nightly jobs on the byte-boundary surfaces (deserializers, network framing, file formats). OSS-Fuzz hosts many Rust crates' targets continuously.
Examples
proptest! {
#[test]
fn roundtrips(d in any::<Duration>()) {
prop_assert_eq!(parse_duration(&format_duration(d)).unwrap(), d);
}
}
// fuzz/fuzz_targets/parse.rs
#![no_main]
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _ = mycrate::parse_duration(s); // must never panic
}
});
cargo +nightly fuzz run parse -- -max_total_time=300
Related
- Testing in Rust — strategies & tooling map — parent map.
- Unit testing — the example-based layer these techniques extend.
- Security testing — fuzzing's role in the security portfolio.
- miri — the UB detector fuzz inputs get replayed under.
- Property-based testing in Java — jqwik; same ideas, JUnit-integrated.
- Property-based testing & fuzzing in Go — Go's native fuzzer vs Rust's cargo-fuzz layer.