Frontend Labs
⋯

23. Testing: unit, integration, doc tests, and property tests

23.1 Unit tests live next to the code

pub fn parse_duration(s: &str) -> Result<u64, String> {
    let (num, unit) = s.split_at(s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()));
    let n: u64 = num.parse().map_err(|_| format!("no number in {s:?}"))?;
    match unit {
        "ms" => Ok(n),
        "s" | "" => Ok(n * 1000),
        "m" => Ok(n * 60_000),
        _ => Err(format!("unknown unit {unit:?}")),
    }
}

#[cfg(test)]                 // compiled only for `cargo test`
mod tests {
    use super::*;

    #[test]
    fn units() {
        for (input, want) in [("250ms", 250), ("3s", 3000), ("2m", 120_000), ("7", 7000)] {
            assert_eq!(parse_duration(input), Ok(want), "input {input:?}"); // table-driven, like Go
        }
    }

    #[test]
    fn rejects_garbage() -> Result<(), String> {   // tests may return Result and use `?`
        assert!(parse_duration("fast").is_err());
        assert!(parse_duration("5h").unwrap_err().contains("unknown unit"));
        Ok(())
    }

    #[test]
    #[should_panic(expected = "overflow")]
    fn overflow_panics_in_debug() {
        let big = parse_duration("18446744073709551615s").unwrap(); // u64::MAX * 1000
        let _ = big;
    }
}

The should_panic test passes under cargo test, which builds in debug mode with overflow checks on. It would fail under cargo test --release, which is a reminder of §3.3: use checked_mul in real code.

23.2 Integration tests and doc tests

mycrate/
├── src/lib.rs
└── tests/               # each file is a separate crate that can only use your PUBLIC API
    └── http_roundtrip.rs
/// Adds two numbers.
///
/// ```
/// assert_eq!(mycrate::add(2, 2), 4);   // this example is COMPILED AND RUN by `cargo test`
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

Doc tests keep the examples in your documentation from going stale.

Command Runs
cargo test everything
cargo test parse_ tests whose name contains parse_
cargo test -- --nocapture show println! output from passing tests
cargo test --doc doc tests only
cargo nextest run a faster runner, one process per test (cargo install cargo-nextest)

23.3 Property-based testing

A table-driven test checks the examples you thought of. A property test generates thousands of inputs and checks a rule that must always hold:

#[cfg(test)]
mod props {
    use proptest::prelude::*;

    fn encode(s: &str) -> String {
        s.bytes().map(|b| format!("{b:02x}")).collect()
    }
    fn decode(h: &str) -> Option<String> {
        let bytes: Option<Vec<u8>> = (0..h.len()).step_by(2)
            .map(|i| h.get(i..i + 2).and_then(|p| u8::from_str_radix(p, 16).ok()))
            .collect();
        String::from_utf8(bytes?).ok()
    }

    proptest! {
        #[test]
        fn roundtrip(s in ".*") {                    // any Unicode string
            prop_assert_eq!(decode(&encode(&s)), Some(s));
        }
        #[test]
        fn decode_never_panics(h in "\\PC*") {       // arbitrary garbage input
            let _ = decode(&h);
        }
    }
}

When a property fails, proptest shrinks the input to the smallest case that still fails. Ch 54 adds fuzzing, which applies the same idea with coverage guidance.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36