Authors property-based tests in Rust using proptest - wires the `proptest!` macro, defines strategies (`prop::collection::vec`, type-driven `any` strategies, regex-based string strategies), uses the strategy-per-value model (vs QuickCheck's per-type) for flexible composition, and exploits proptest's automatic shrinking + persistence of failed cases (regression test artifact). Use when a Rust codebase needs PBT - pairs especially well with parsers, serializers, and any function with a structured input domain.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Per proptest-readme:
"Proptest is a property testing framework inspired by Python's Hypothesis. It automatically generates test inputs and shrinks failures to minimal cases."
"Strategy-based approach: Unlike QuickCheck, generation is 'defined on a per-value basis instead of per-type,' enabling flexible composition." (proptest-readme)
The strategy-based design is the key differentiator from Rust's
quickcheck crate - proptest separates the generation strategy
from the type, so multiple strategies can produce the same type
without newtype wrappers.
"Failure persistence: Failed test cases are saved for regression testing." (proptest-readme)
This means a failed property test produces a proptest-regressions/
file checked into git; future runs replay it before generating new
cases.
decode(encode(x)) == x, sort(sort(x)) == sort(x)).In Cargo.toml:
[dev-dependencies]
proptest = "1"Note: per proptest-readme, "the crate mainly sees passive maintenance" - the API is stable; new development is rare.
Per proptest-readme:
use proptest::prelude::*;
proptest! {
#[test]
fn parses_valid_dates(s in "[0-9]{4}-[0-9]{2}-[0-9]{2}") {
parse_date(&s).unwrap();
}
}The proptest! macro wraps a #[test]-style function. The
parameter declaration s in "..." says "generate s from the
regex strategy."
Per proptest-readme, one of proptest's distinguishing
features is regex-based string generation: "[0-9]{4}-[0-9]{2}-[0-9]{2}"
generates date-shaped strings without writing a custom strategy.
| Strategy | Generates |
|---|---|
any::<T>() | Any value of type T (uses default strategy) |
0..100i32 | Integers in range |
"[a-z]+" (regex string) | Strings matching regex |
prop::collection::vec(strategy, 0..10) | Vec of length 0-10 |
prop::collection::hash_map(k, v, 0..10) | HashMap |
prop::collection::btree_set(s, 0..10) | BTreeSet |
(s1, s2, s3) (tuple) | Tuple of values |
Just(value) | Constant |
prop_oneof![s1, s2, s3] | One of multiple strategies |
s.prop_map(f) | Transform via function |
s.prop_filter("reason", f) | Filter (with reason for shrinking) |
s.prop_flat_map(f) | Dependent generation |
For custom domain strategies with prop_compose! and the
round-trip / invariant / equivalence property patterns they feed,
see
references/strategies-and-patterns.md.
proptest! {
#![proptest_config(ProptestConfig {
cases: 1000, // default 256
max_shrink_iters: 100, // default 1024
..ProptestConfig::default()
})]
#[test]
fn expensive_property(x in any::<u64>()) {
// ...
}
}Common config options:
| Field | Default | Use |
|---|---|---|
cases | 256 | More for higher confidence; slower CI. |
max_shrink_iters | 1024 | Cap shrinking time on slow tests. |
max_shrink_time | 0 | Time-based shrink cap (ms; 0 = unlimited). |
verbose | 0 | 0 / 1 / 2 - verbosity level. |
failure_persistence | enabled | Persist failed cases to proptest-regressions/. |
When a property fails, proptest writes:
# proptest-regressions/my_module.txt
# Seeds for failure cases proptest has generated in the past.
# It is recommended to commit this file.
cc abc1234567890 # shrinks to s = "1900-02-30"Commit this file to git. Future runs replay these seeds before generating new cases - locks the regression in.
If the regression is no longer relevant (the bug was fixed via a different code path that doesn't fail this case anymore), delete the entry. Don't suppress failures; understand them first.
- run: cargo test --workspaceThat's it - proptest tests are regular #[test] functions wrapped
in the macro. Cargo's --test-threads and parallel test execution
work normally.
For deterministic CI, set the seed via env var:
env:
PROPTEST_SEED: 0xDEADBEEF
- run: cargo testproptest = "1" under [dev-dependencies] in Cargo.toml.proptest! { #[test] fn ... (x in <strategy>) { ... } }
form; pick strategies from the catalog (regex strings, ranges,
prop::collection::*).prop_compose! -
references/strategies-and-patterns.md.prop_assert! / prop_assert_eq! (not vanilla
assert!) so the shrinker sees the failure context.cargo test; on failure, commit the generated
proptest-regressions/ file so the seed replays on later runs.cases / max_shrink_iters via #![proptest_config(...)]
when a test is slow.An RgbColor type exposes to_hex() and from_hex(); the team
claims from_hex(c.to_hex()) == c for every color.
prop_compose!
(references/strategies-and-patterns.md):use proptest::prelude::*;
prop_compose! {
fn rgb()(r in any::<u8>(), g in any::<u8>(), b in any::<u8>()) -> RgbColor {
RgbColor { r, g, b }
}
}prop_assert_eq!:proptest! {
#[test]
fn hex_round_trip(c in rgb()) {
prop_assert_eq!(RgbColor::from_hex(&c.to_hex()), c);
}
}to_hex() emits lowercase but from_hex() rejects it, so the
run fails, proptest shrinks, and writes a regression file:# proptest-regressions/color.txt
cc 1a2b3c4d # shrinks to c = RgbColor { r: 0, g: 0, b: 10 }proptest-regressions/color.txt (see Failure
persistence). Fix from_hex() to accept lowercase; re-run
cargo test and the property passes all 256 cases, replaying the
saved seed first.| Anti-pattern | Why it fails | Fix |
|---|---|---|
.gitignore proptest-regressions/ | Loses regression artifacts; future runs may not catch the same bug. | Commit the directory (Failure persistence). |
Heavy prop_filter on broad strategies | Slow generation; many cases discarded. | Constrained strategies (Strategy catalog, e.g. 1u32..100u32 instead of any().filter). |
assert! instead of prop_assert! inside the macro | Shrinker can't see the failure context; minimal case obscured. | Always prop_assert! / prop_assert_eq! (strategies-and-patterns.md). |
Bare unwrap() inside the property without context | Error message is unhelpful; shrinker can't aid debugging. | Use prop_assert!(result.is_ok()) or let v = result.unwrap() after asserting. |
cases = 100_000 for a slow test | CI never finishes; team disables. | Budget appropriate to test runtime. |
| Mocking external services inside the property | Mocks don't satisfy properties. | Test pure functions; integration tests separate. |
| Random seed in CI | Failures hard to reproduce. | PROPTEST_SEED env var (CI integration). |
max_shrink_iters /
max_shrink_time cap it.proptest! for async fn; wrap with tokio::test and
block_on.fc.scheduler,
proptest doesn't model concurrent interleavings out of the box.prop_compose! custom strategies and round-trip / invariant /
equivalence property patterns.hypothesis-testing - Python
sibling proptest is inspired by.fast-check-testing,
jqwik-testing,
quickcheck-testing -
per-language siblings.