TL;DR: Property-based testing generates hundreds of random inputs and verifies that a property holds for all of them; instead of testing specific examples you chose manually.
proptestis the most popular property-based testing crate in Rust. Write aproptest!block, define input strategies (ranges, regex, arbitrary structs), and let proptest find failures. When it finds a failure, it "shrinks" the input to the smallest failing case automatically. Great for finding edge cases, overflow bugs, and round-trip invariants.
What Is Property-Based Testing?
Instead of "input X gives output Y", you assert "for ALL inputs in this domain, this property holds"; proptest generates the inputs.
# Cargo.toml
[dev-dependencies]
proptest = "1"// Traditional unit test; you pick the examples
#[test]
fn test_reverse() {
assert_eq!(reverse_string("hello"), "olleh");
assert_eq!(reverse_string(""), "");
assert_eq!(reverse_string("a"), "a");
// What about Unicode? Long strings? Null bytes?
}
// Property test; proptest picks thousands of examples
use proptest::prelude::*;
proptest! {
#[test]
fn reverse_twice_is_identity(s in ".*") {
// For any string, reversing twice gives the original
let result = reverse_string(&reverse_string(&s));
prop_assert_eq!(result, s);
}
}How Do You Write Basic Property Tests?
use proptest::prelude::*;
fn add(a: i32, b: i32) -> i32 { a + b }
fn sort(mut v: Vec<i32>) -> Vec<i32> { v.sort(); v }
proptest! {
// Arithmetic properties
#[test]
fn add_is_commutative(a in i32::MIN/2..=i32::MAX/2, b in i32::MIN/2..=i32::MAX/2) {
prop_assert_eq!(add(a, b), add(b, a));
}
#[test]
fn add_is_associative(a in -1000i32..1000, b in -1000i32..1000, c in -1000i32..1000) {
prop_assert_eq!(add(add(a, b), c), add(a, add(b, c)));
}
// Collection properties
#[test]
fn sorted_length_unchanged(v in prop::collection::vec(any::<i32>(), 0..100)) {
let sorted = sort(v.clone());
prop_assert_eq!(sorted.len(), v.len());
}
#[test]
fn sorted_is_ordered(v in prop::collection::vec(any::<i32>(), 0..100)) {
let sorted = sort(v);
for window in sorted.windows(2) {
prop_assert!(window[0] <= window[1]);
}
}
}How Do You Generate Custom Types?
Use #[derive(Arbitrary)] for simple types, or compose strategies for complex ones.
use proptest::prelude::*;
use proptest_derive::Arbitrary;
#[derive(Debug, Arbitrary)]
struct User {
#[proptest(regex = "[a-z]{3,20}")]
username: String,
#[proptest(strategy = "18u32..=120")]
age: u32,
active: bool,
}
proptest! {
#[test]
fn user_validation_never_panics(user in any::<User>()) {
// Ensure validation handles any input without panicking
let _ = validate_user(&user);
}
}Manual strategy composition:
fn valid_email() -> impl Strategy<Value = String> {
(r"[a-z]{3,10}", r"[a-z]{3,10}", r"(com|org|net)")
.prop_map(|(user, domain, tld)| format!("{user}@{domain}.{tld}"))
}
proptest! {
#[test]
fn email_parser_accepts_valid_emails(email in valid_email()) {
assert!(parse_email(&email).is_ok(), "rejected valid email: {email}");
}
}How Does Shrinking Work?
When proptest finds a failing input, it automatically reduces it to the smallest case that still fails; making bugs easy to debug.
Failure found with input: "xgqmplwzabcdefgh..."
Shrinking...
Minimal failing input: "abc"This is one of the key advantages over manual fuzzing; you get a minimal reproduction immediately.
What Are Common Property Test Patterns?
| Pattern | What to assert |
|---|---|
| Round-trip | decode(encode(x)) == x |
| Inverse operations | decrypt(encrypt(x, k), k) == x |
| Idempotence | f(f(x)) == f(x) |
| Monotonicity | if a <= b then f(a) <= f(b) |
| No panic | let _ = risky_function(x) |
| Length preservation | transform(v).len() == v.len() |
| Commutativity | f(a, b) == f(b, a) |
Frequently Asked Questions
Both are property-based testing libraries. proptest has more powerful strategies (regex-based string generation, better shrinking, derives), richer composition, and is more actively maintained. quickcheck is simpler and older. For new projects, prefer proptest.
256 by default. Configure with ProptestConfig:
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn my_test(x in any::<i32>()) { ... }
}Yes; it saves the seed and minimal failing case in a .proptest-regressions directory so CI always re-runs known failures.
Not directly in proptest! blocks. Run async code with a local runtime:
proptest! {
#[test]
fn async_property(input in any::<String>()) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { my_async_fn(&input).await });
}
}Sources
Related Glossary Terms
- Result: Property tests often verify Result invariants
- Iterator: proptest strategies are iterator-like
- Enum:
#[derive(Arbitrary)]works on enums too - Criterion: Property tests and benchmarks pair well when validating both correctness and performance envelopes
- Rand: Generated test cases are conceptually close to randomized input generation workflows
Keep Reading
- Rust Error Handling: Result and Option: property tests are most valuable for error path coverage
- Learn Rust in 2026: Rust's testing culture and tooling

