Testland
Browse all skills & agents

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 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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill proptest-testing
View source

proptest-testing

Overview

Per proptest-readme (opens in new window):

"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 (opens in new window))

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 (opens in new window))

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 (opens in new window), "the crate mainly sees passive maintenance" - the API is stable; new development is rare.

Basic property

Per proptest-readme (opens in new window):

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 (opens in new window), 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

StrategyGenerates
any::<T>()Any value of type T (uses default strategy)
0..100i32Integers 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:

FieldDefaultUse
cases256More for higher confidence; slower CI.
max_shrink_iters1024Cap shrinking time on slow tests.
max_shrink_time0Time-based shrink cap (ms; 0 = unlimited).
verbose00 / 1 / 2 - verbosity level.
failure_persistenceenabledPersist 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

  1. Add proptest = "1" under [dev-dependencies] in Cargo.toml.
  2. Identify a property that holds for all inputs (round-trip, invariant, or equivalence), not a single example.
  3. Declare inputs in the proptest! { #[test] fn ... (x in <strategy>) { ... } } form; pick strategies from the catalog (regex strings, ranges, prop::collection::*).
  4. For structured domain types, build a custom strategy with prop_compose! - references/strategies-and-patterns.md.
  5. Assert with prop_assert! / prop_assert_eq! (not vanilla assert!) so the shrinker sees the failure context.
  6. Run cargo test; on failure, commit the generated proptest-regressions/ file so the seed replays on later runs.
  7. Tune cases / max_shrink_iters via #![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.

  1. 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 }
    }
}
  1. 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);
    }
}
  1. 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 }
  1. Commit 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-patterns

Anti-patternWhy it failsFix
.gitignore proptest-regressions/Loses regression artifacts; future runs may not catch the same bug.Commit the directory (Failure persistence).
Heavy prop_filter on broad strategiesSlow generation; many cases discarded.Constrained strategies (Strategy catalog, e.g. 1u32..100u32 instead of any().filter).
assert! instead of prop_assert! inside the macroShrinker 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 contextError 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 testCI never finishes; team disables.Budget appropriate to test runtime.
Mocking external services inside the propertyMocks don't satisfy properties.Test pure functions; integration tests separate.
Random seed in CIFailures hard to reproduce.PROPTEST_SEED env var (CI integration).

Limitations

  • Maintenance status. Per proptest-readme (opens in new window), "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_time cap it.
  • Async testing requires manual integration. No native proptest! for async fn; wrap with tokio::test and block_on.
  • No race-condition detection. Unlike fast-check's fc.scheduler, proptest doesn't model concurrent interleavings out of the box.

References

  • pt (opens in new window) - 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.

proptest custom strategies and property patterns

View source (opens in new window)

proptest custom strategies and property patterns

Building domain strategies with prop_compose! and the round-trip / invariant / equivalence property patterns they feed.

Custom strategies via prop_compose!

use proptest::prelude::*;

prop_compose! {
    fn valid_user()(
        id in 1u64..1_000_000u64,
        email in "[a-z]{3,10}@(example|test)\\.com",
        age in 18u32..100u32,
    ) -> User {
        User { id, email, age }
    }
}

proptest! {
    #[test]
    fn user_serialization_round_trip(u in valid_user()) {
        let json = serde_json::to_string(&u).unwrap();
        let back: User = serde_json::from_str(&json).unwrap();
        prop_assert_eq!(u, back);
    }
}

prop_compose! builds a strategy from multiple sub-strategies; the body returns a constructed value. The () after the function name is for non-generated parameters (rare).

Round-trip and invariant patterns

Per proptest-readme (opens in new window): property testing checks "that certain properties of your code hold for arbitrary inputs."

// Round-trip
proptest! {
    #[test]
    fn json_round_trip(v in any::<Value>()) {
        let json = serde_json::to_string(&v).unwrap();
        let parsed: Value = serde_json::from_str(&json).unwrap();
        prop_assert_eq!(v, parsed);
    }
}

// Invariant - sort produces sorted output
proptest! {
    #[test]
    fn sorted_is_sorted(mut v in prop::collection::vec(any::<i32>(), 0..1000)) {
        v.sort();
        for window in v.windows(2) {
            prop_assert!(window[0] <= window[1]);
        }
    }
}

// Equivalence - new impl matches old
proptest! {
    #[test]
    fn new_matches_old(input in any::<Input>()) {
        prop_assert_eq!(new_implementation(&input), old_implementation(&input));
    }
}

prop_assert! and prop_assert_eq! integrate with the shrinker better than vanilla assert! - use them inside proptest! blocks.

Related skills

fast-check-testing

Authors property-based tests in JavaScript / TypeScript using fast-check - wires `fc.assert(fc.property(arbitrary, ...))`, picks arbitraries (`fc.integer`, `fc.string`, `fc.array`, `fc.tuple`, `fc.record`), uses `.map()` / `.chain()` / `.filter()` to build domain arbitraries, and integrates with Jest / Vitest / Mocha / Jasmine / AVA / Tape. Use when a JS/TS codebase needs PBT to catch edge cases - fast-check has been used to find bugs in major libraries (`query-string`, etc.) and is trusted by Jest, Jasmine, fp-ts, Ramda.

hypothesis-testing

Authors property-based tests in Python using Hypothesis - wires `@given` with `strategies` (`st.integers`, `st.text`, `st.lists`, `st.from_regex`, `st.composite`), uses `assume()` / `.filter()` for preconditions, configures via `@settings(max_examples=..., deadline=...)`, and exploits Hypothesis's automatic shrinking to find the falsifying example. Integrates with pytest fixtures + parametrize. Use when a Python project needs PBT to catch edge cases the example-based tests miss - bug clusters around input ranges / boundary values / interaction between fields.

jqwik-testing

Authors property-based tests for the JVM (Java + Kotlin) using jqwik - wires `@Property` test methods, `@ForAll` parameter annotations, `Arbitraries.integers/strings/etc` generators, custom `@Provide` arbitraries, and the JUnit 5 platform integration. Use when a JVM project needs PBT - alternative to JUnit-QuickCheck and Vavr's property-checking; tightly integrates with JUnit 5 so property tests run alongside conventional unit tests in the same Maven / Gradle pipeline.

quickcheck-testing

Authors property-based tests for Haskell using QuickCheck (the original PBT library) and for Scala via ScalaCheck (the JVM port) - wires `quickCheck` (Haskell) / `forAll` (ScalaCheck) drivers, defines `Arbitrary` instances or generators, uses `shrink` to find minimal counterexamples, and integrates with HSpec / Tasty (Haskell) or specs2 / ScalaTest. Use when the codebase is Haskell or Scala and the team wants the canonical PBT library that the entire family (Hypothesis / fast-check / proptest / jqwik) was inspired by.