proptest-testing
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 s
Files3
View on GitHubSKILL.mdplugins/qa-property-based/skills/proptest-testing/SKILL.md
GitHubproptest-testing
Overview
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.
When to use
- A Rust codebase has functions with structured inputs (parsers, serializers, validators, encoders).
- A bug fix needs property-level prevention (not just an example test).
- Round-trip / invariant properties hold
(
decode(encode(x)) == x,sort(sort(x)) == sort(x)). - A refactor needs equivalence verification (new vs old produce same outputs for all inputs).
Install
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.
Basic property
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 catalog
| 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.
Configuration
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/. |
Failure persistence (the regression file)
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.
CI integration
- run: cargo test --workspace
That'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 test
How to use
- Add
proptest = "1"under[dev-dependencies]inCargo.toml. - Identify a property that holds for all inputs (round-trip, invariant, or equivalence), not a single example.
- Declare inputs in the
proptest! { #[test] fn ... (x in <strategy>) { ... } }form; pick strategies from the catalog (regex strings, ranges,prop::collection::*). - For structured domain types, build a custom strategy with
prop_compose!- references/strategies-and-patterns.md. - Assert with
prop_assert!/prop_assert_eq!(not vanillaassert!) so the shrinker sees the failure context. - Run
cargo test; on failure, commit the generatedproptest-regressions/file so the seed replays on later runs. - Tune
cases/max_shrink_itersvia#![proptest_config(...)]when a test is slow.
Worked example
An RgbColor type exposes to_hex() and from_hex(); the team
claims from_hex(c.to_hex()) == c for every color.
- Build a strategy for arbitrary colors with
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 }
}
}
- Assert the round-trip with
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 butfrom_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 }
- Commit
proptest-regressions/color.txt(see Failure persistence). Fixfrom_hex()to accept lowercase; re-runcargo testand the property passes all 256 cases, replaying the saved seed first.
Anti-patterns
| 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). |
Limitations
- Maintenance status. Per proptest-readme, "the crate mainly sees passive maintenance." Expect bug fixes but not new features.
- MSRV 1.84. Older Rust toolchains can't use the latest version.
- Shrinking on complex types is slow.
max_shrink_iters/max_shrink_timecap it. - Async testing requires manual integration. No native
proptest!forasync fn; wrap withtokio::testandblock_on. - No race-condition detection. Unlike fast-check's
fc.scheduler, proptest doesn't model concurrent interleavings out of the box.
References
- pt - proptest README: strategy-based approach, regex-string generation, failure persistence, comparison vs QuickCheck, MSRV, license.
- references/strategies-and-patterns.md -
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.