Back to skills
extension
Category: Development & EngineeringNo API key required

proptest

Property-based testing for Rust using proptest. Use when writing tests that verify properties hold for arbitrary inputs, implementing custom strategies for generating test data, testing edge cases automatically, or when the user mentions "proptest", "property-based testing", "fuzz testing in Rust", "shrinking", or needs to generate arbitrary test data. Triggers include requests to test functions with random inputs, find edge cases, implement Arbitrary for custom types, or create data generators.

personAuthor: jakexiaohubgithub

Proptest - Property-Based Testing for Rust

Proptest tests properties that hold for arbitrary inputs, automatically finding minimal failing cases through shrinking.

Setup

# Cargo.toml
[dev-dependencies]
proptest = "1.0"
proptest-derive = "0.5"  # Optional: for #[derive(Arbitrary)]

Core Patterns

Basic Property Test

use proptest::prelude::*;

proptest! {
    #[test]
    fn my_property(input in 0..100i32) {
        // Property assertions go here
        prop_assert!(input >= 0);
        prop_assert!(input < 100);
    }
}

Multiple Inputs

proptest! {
    #[test]
    fn test_addition_commutative(a in any::<i32>(), b in any::<i32>()) {
        prop_assert_eq!(a.wrapping_add(b), b.wrapping_add(a));
    }
}

Strategies

Strategies define how to generate test values. Core strategy types:

| Strategy | Generates | Example | |----------|-----------|---------| | any::<T>() | Any value of type T | any::<u32>() | | a..b | Range values | 0..100i32 | | "regex" | Strings matching regex | "[a-z]{1,10}" | | Just(v) | Always returns v | Just(42) | | prop_oneof![...] | One of multiple strategies | prop_oneof![Just(1), Just(2)] |

String Strategies

// Regex-based strings
"[a-zA-Z0-9]{1,20}"           // Alphanumeric, 1-20 chars
"\\PC*"                        // Any non-control chars
"[0-9]{4}-[0-9]{2}-[0-9]{2}"  // Date format YYYY-MM-DD

// Any string
any::<String>()

Collection Strategies

use proptest::collection;

// Vectors
collection::vec(any::<i32>(), 0..100)     // Vec<i32> with 0-99 elements
collection::vec("[a-z]+", 1..10)          // Vec<String> of words

// Other collections
collection::hash_map(any::<String>(), any::<i32>(), 0..10)
collection::btree_set(0..1000i32, 5..20)

Strategy Combinators

prop_map - Transform Values

fn point_strategy() -> impl Strategy<Value = Point> {
    (any::<f32>(), any::<f32>())
        .prop_map(|(x, y)| Point { x, y })
}

prop_filter - Reject Values (Use Sparingly)

fn even_numbers() -> impl Strategy<Value = i32> {
    any::<i32>().prop_filter("must be even", |x| x % 2 == 0)
}

// BETTER: Generate directly instead of filtering
fn even_numbers_better() -> impl Strategy<Value = i32> {
    any::<i32>().prop_map(|x| x * 2)
}

prop_flat_map - Dependent Strategies

Use when one generated value determines how to generate another:

// Generate a vec and a valid index into it
fn vec_and_index() -> impl Strategy<Value = (Vec<i32>, usize)> {
    collection::vec(any::<i32>(), 1..100)
        .prop_flat_map(|vec| {
            let len = vec.len();
            (Just(vec), 0..len)
        })
}

prop_compose! Macro

Cleaner syntax for composing strategies:

prop_compose! {
    fn arb_order(max_qty: u32)
        (id in any::<u32>(), item in "[a-z]+", qty in 1..max_qty)
        -> Order
    {
        Order { id: id.to_string(), item, quantity: qty }
    }
}

// Three-arg form for dependent generation
prop_compose! {
    fn vec_and_index()
        (vec in collection::vec(any::<i32>(), 1..100))
        (index in 0..vec.len(), vec in Just(vec))
        -> (Vec<i32>, usize)
    {
        (vec, index)
    }
}

Deriving Arbitrary

use proptest_derive::Arbitrary;

#[derive(Debug, Arbitrary)]
struct User {
    #[proptest(regex = "[a-z]{3,10}")]
    name: String,
    #[proptest(strategy = "18..120u8")]
    age: u8,
}

#[derive(Debug, Arbitrary)]
enum Status {
    Active,
    #[proptest(weight = 3)]  // 3x more likely
    Pending,
    #[proptest(skip)]        // Never generated
    Deleted,
}

Recursive Data Structures

fn arb_tree() -> impl Strategy<Value = Tree> {
    let leaf = any::<i32>().prop_map(Tree::Leaf);

    leaf.prop_recursive(
        8,    // max depth
        256,  // max nodes
        10,   // expected branch size
        |inner| {
            collection::vec(inner, 0..10)
                .prop_map(Tree::Branch)
        }
    )
}

Assertions

| Macro | Purpose | |-------|---------| | prop_assert!(cond) | Assert condition | | prop_assert_eq!(a, b) | Assert equality | | prop_assert_ne!(a, b) | Assert inequality | | prop_assume!(cond) | Skip test case if false (global filter) |

Use prop_assert* instead of regular assert* - they produce cleaner output during shrinking.

Configuration

proptest! {
    #![proptest_config(ProptestConfig {
        cases: 1000,           // Number of test cases (default: 256)
        max_shrink_iters: 100, // Max shrink attempts (default: unlimited)
        .. ProptestConfig::default()
    })]

    #[test]
    fn configured_test(x in any::<i32>()) {
        prop_assert!(true);
    }
}

Failure Persistence

Failed test cases are saved to proptest-regressions/ directory. Add to version control:

git add proptest-regressions/

Best Practices

  1. Prefer generation over filtering - Filtering slows tests and harms shrinking
  2. Use prop_assert* macros - Better error output than assert*
  3. Keep strategies deterministic - Same seed should produce same values
  4. Add regression files to VCS - Ensures bugs stay fixed
  5. Start simple, add complexity - Begin with any::<T>(), constrain as needed
  6. Test properties, not implementations - Focus on invariants: reverse(reverse(x)) == x

Common Patterns

Roundtrip Testing

proptest! {
    #[test]
    fn serialization_roundtrip(original in any::<MyStruct>()) {
        let serialized = serde_json::to_string(&original)?;
        let deserialized: MyStruct = serde_json::from_str(&serialized)?;
        prop_assert_eq!(original, deserialized);
    }
}

Testing Invariants

proptest! {
    #[test]
    fn sorted_output_is_sorted(mut vec in collection::vec(any::<i32>(), 0..100)) {
        vec.sort();
        for window in vec.windows(2) {
            prop_assert!(window[0] <= window[1]);
        }
    }
}

Oracle Testing (Compare Implementations)

proptest! {
    #[test]
    fn fast_matches_slow(input in any::<Input>()) {
        prop_assert_eq!(fast_implementation(&input), slow_reference(&input));
    }
}

For advanced patterns (state machine testing, no_std support, async), see references/advanced.md.