rgoussu@goussu: ~/library/rust/testing
~/library/rust/testing cat property-based-and-fuzzing.md

Property-based testing & fuzzing — proptest & cargo-fuzz

# Invariants over generated inputs with proptest (strategies, shrinking, regression files) and coverage-guided fuzzing with cargo-fuzz/libFuzzer and the arbitrary crate — and how the two share targets.

Conceptsaved 2026-08-09 #rust#testing#proptest#fuzzing#cargo-fuzz

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 init scaffolds fuzz/fuzz_targets/*.rs; each target is a fuzz_target!(|data: &[u8]| { … }) closure built with libFuzzer + AddressSanitizer (nightly), corpus-managed under fuzz/corpus. AFL via afl.rs is the alternative engine; structure-aware fuzzing comes from taking |input: MyType| with arbitrary.
  • 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