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.