protest 1.1.0

An ergonomic, powerful, and feature-rich property testing library with minimal boilerplate.
Documentation

โœŠ Protest โ€ƒ Build Status Latest Version Documentation

Property-Based Testing for Rust - An ergonomic, powerful, and feature-rich property testing library with minimal boilerplate.

Features

  • ๐Ÿš€ Ergonomic API - Test properties with closures, no boilerplate
  • ๐ŸŽฏ Automatic Generator Inference - Smart type-based generator selection
  • ๐Ÿ”ง Derive Macros - #[derive(Generator)] for custom types
  • ๐Ÿ“ฆ Declarative Macros - property!, assert_property!, generator!
  • โšก Async Support - First-class async property testing
  • ๐Ÿ”„ Smart Shrinking - Automatic minimal counterexample finding
  • ๐Ÿ’พ Failure Persistence - Save and replay failing test cases (optional)
  • ๐Ÿ”ง CLI Tool - Manage failures from the command line (protest-cli)
  • ๐ŸŽจ Fluent Builders - Chain configuration methods naturally
  • ๐Ÿงช Common Patterns - Built-in helpers for mathematical properties
  • ๐Ÿ”€ Parallel Execution - Run tests in parallel for speed
  • ๐Ÿ“Š Statistics & Coverage - Track generation and test coverage
  • ๐ŸŽญ Flexible - Works with any type, sync or async

Quick Start

Add Protest to your Cargo.toml:

[dev-dependencies]
protest = { version = "*", features = ["derive", "persistence"] }

Optional Extensions:

protest-extras = "*"           # Extra generators (network, datetime, text)
protest-stateful = "*"         # Stateful testing & model checking
protest-criterion = "*"        # Property-based benchmarking
protest-insta = "*"            # Snapshot testing integration
protest-proptest-compat = "*"  # Migration helpers from proptest

See individual package READMEs for detailed documentation:

Ultra-Simple Example

use protest::*;

#[test]
fn test_addition_commutative() {
    // Test that addition is commutative with just one line!
    property!(generator!(i32, -100, 100), |(a, b)| a + b == b + a);
}

Ergonomic API Example

use protest::ergonomic::*;

#[test]
fn test_reverse_twice_is_identity() {
    property(|mut v: Vec<i32>| {
        let original = v.clone();
        v.reverse();
        v.reverse();
        v == original
    })
    .iterations(1000)
    .run_with(VecGenerator::new(IntGenerator::new(-50, 50), 0, 100))
    .expect("Property should hold");
}

Attribute Macro Example

use protest::property_test;

#[property_test(iterations = 100)]
fn test_string_length(s: String) {
    // Generator automatically inferred from type
    assert!(s.len() >= 0);
}

Custom Struct Example

use protest::Generator;

#[derive(Debug, Clone, PartialEq, Generator)]
struct User {
    #[generator(range = "1..1000")]
    id: u32,

    #[generator(length = "5..50")]
    name: String,

    age: u8,
    active: bool,
}

#[property_test]
fn test_user_id(user: User) {
    assert!(user.id > 0 && user.id < 1000);
}

API Styles

Protest offers multiple API styles - use what fits your needs:

1. Declarative Macros (Most Concise)

use protest::*;

// Simple property test
property!(generator!(i32, 0, 100), |x| x >= 0);

// With configuration
property!(
    generator!(i32, 0, 100),
    iterations = 1000,
    seed = 42,
    |x| x >= 0
);

// Assert style (panics on failure)
assert_property!(
    generator!(i32, 0, 100),
    |x| x * 2 > x,
    "Doubling should increase positive numbers"
);

2. Fluent Builder API (Most Flexible)

use protest::ergonomic::*;

property(|x: i32| x.abs() >= 0)
    .iterations(1000)
    .seed(42)
    .max_shrink_iterations(500)
    .run_with(IntGenerator::new(-100, 100))
    .expect("Absolute value is always non-negative");

3. Attribute Macros (Most Integrated)

use protest::property_test;

#[property_test(iterations = 100, seed = 42)]
fn test_vec_operations(v: Vec<i32>) {
    let mut sorted = v.clone();
    sorted.sort();
    assert!(sorted.windows(2).all(|w| w[0] <= w[1]));
}

4. Direct API (Most Control)

use protest::*;

struct MyProperty;
impl Property<i32> for MyProperty {
    type Output = ();
    fn test(&self, input: i32) -> Result<(), PropertyError> {
        if input >= 0 {
            Ok(())
        } else {
            Err(PropertyError::property_failed("negative number"))
        }
    }
}

let result = check(IntGenerator::new(0, 100), MyProperty);
assert!(result.is_ok());

Common Property Patterns

Protest includes built-in helpers for common mathematical properties:

use protest::ergonomic::patterns::*;

// Commutativity: f(a, b) == f(b, a)
commutative(|a: i32, b: i32| a + b);

// Associativity: f(f(a, b), c) == f(a, f(b, c))
associative(|a: i32, b: i32| a + b);

// Idempotence: f(f(x)) == f(x)
idempotent(|x: i32| x.abs());

// Round-trip: decode(encode(x)) == x
round_trip(
    |x: i32| x.to_string(),
    |s: String| s.parse().unwrap()
);

// Inverse functions: f(g(x)) == x && g(f(x)) == x
inverse(|x: i32| x * 2, |x: i32| x / 2);

// Identity element: f(x, e) == x
has_identity(|a: i32, b: i32| a + b, 0);

// Monotonicity
monotonic_increasing(|x: i32| x * x);

// Distributivity
distributive(
    |a: i32, b: i32| a * b,
    |a: i32, b: i32| a + b
);

Async Support

Full support for runtime-agnostic async property testing. Works with any async runtime (tokio, async-std, smol):

use protest::*;

struct AsyncFetchProperty;

impl AsyncProperty<u32> for AsyncFetchProperty {
    type Output = ();

    async fn test(&self, id: u32) -> Result<(), PropertyError> {
        let user = fetch_user(id).await;
        if id > 0 && user.is_none() {
            Err(PropertyError::property_failed("User not found"))
        } else {
            Ok(())
        }
    }
}

#[tokio::test]
async fn test_async_property() {
    let result = check_async(
        IntGenerator::new(1, 100),
        AsyncFetchProperty
    ).await;

    assert!(result.is_ok());
}

Note: Protest is runtime-agnostic - you bring your own async runtime. Add tokio, async-std, or smol to your dev-dependencies as needed.

Automatic Generator Inference

Protest automatically infers generators for common types:

use protest::ergonomic::AutoGen;

// All primitive types
i32::auto_generator();
String::auto_generator();
bool::auto_generator();

// Collections
Vec::<i32>::auto_generator();
HashMap::<String, i32>::auto_generator();

// Tuples
<(i32, String)>::auto_generator();

// Options
Option::<i32>::auto_generator();

// Your custom types with #[derive(Generator)]
User::auto_generator();

Shrinking

When a property fails, Protest automatically finds the minimal counterexample:

property!(generator!(i32, 1, 100), |x| x < 50);
// Fails with: Property failed with input 50 (shrunk from larger value)
//           Focus on input: 50

Configuration

Extensive configuration options:

use protest::*;
use std::time::Duration;

let config = TestConfig {
    iterations: 1000,                            // Number of test cases
    seed: Some(42),                               // For reproducibility
    max_shrink_iterations: 500,                  // Shrinking limit
    shrink_timeout: Duration::from_secs(10),     // Shrinking timeout
    generator_config: GeneratorConfig {
        size_hint: 100,                          // Size for collections
        max_depth: 5,                            // For nested structures
        ..GeneratorConfig::default()
    },
    ..TestConfig::default()
};

Failure Persistence & Replay

Save failing test cases and automatically replay them (requires persistence feature):

use protest::*;

PropertyTestBuilder::new()
    .test_name("my_critical_test")
    .persist_failures()  // Enable automatic failure saving & replay
    .iterations(10000)
    .run(u32::arbitrary(), |x: u32| {
        // Your property test
        if x > 1000 {
            Err("Value too large")
        } else {
            Ok(())
        }
    });

What happens:

  1. Failed tests are automatically saved to .protest/failures/
  2. On subsequent runs, failures are replayed before running new cases
  3. Fixed failures are automatically cleaned up

Install the CLI tool for advanced failure management:

cargo install protest-cli

See the CLI documentation for complete details on managing failures, generating regression tests, and corpus building.

Stateful Property Testing

Test state machines, databases, and concurrent systems with protest-stateful:

use protest_stateful::{Operation, prelude::*};

#[derive(Debug, Clone, Operation)]
#[operation(state = "Vec<i32>")]
enum StackOp {
    #[execute("state.push(*field_0)")]
    #[weight(5)]
    Push(i32),

    #[execute("state.pop()")]
    #[precondition("!state.is_empty()")]
    Pop,
}

Features:

  • State machine testing with derive macros
  • Model-based testing (compare against reference implementation)
  • Temporal properties (Always, Eventually)
  • Linearizability verification for concurrent systems

See protest-stateful README for complete documentation.

Examples

The repository includes comprehensive examples:

Run examples:

cargo run --example ergonomic_api_demo
cargo run --example custom_structs
cargo run --example async_properties

Property-Based Benchmarking

Benchmark with diverse generated inputs using protest-criterion:

use criterion::Criterion;
use protest_criterion::PropertyBencher;

fn bench_sort(c: &mut Criterion) {
    c.bench_property("vec sort", vec_generator, |v: &Vec<i32>| {
        let mut sorted = v.clone();
        sorted.sort();
    }, 100);
}

See protest-criterion README for details.

Property-Based Snapshot Testing

Visual regression testing with protest-insta:

use protest_insta::PropertySnapshots;

#[test]
fn test_report_snapshots() {
    let mut snapshots = PropertySnapshots::new("reports");

    for report in generate_reports() {
        snapshots.assert_json_snapshot(&report);
    }
}

See protest-insta README for details.

Migrating from Proptest

Use protest-proptest-compat for migration helpers:

Before (Proptest)

proptest! {
    #[test]
    fn test_addition(a in 0..100i32, b in 0..100i32) {
        assert!(a + b >= a && a + b >= b);
    }
}

After (Protest)

#[test]
fn test_addition() {
    property!(generator!(i32, 0, 100), |(a, b)| {
        a + b >= a && a + b >= b
    });
}

See protest-proptest-compat README for the complete migration guide.

Feature Flags

[features]
default = ["derive"]
derive = ["protest-derive"]    # Derive macros for Generator trait
persistence = ["serde", "serde_json"]  # Failure persistence & replay

Protest has minimal dependencies and no required runtime dependencies. Async support is built-in and runtime-agnostic. The persistence feature is optional and adds serde for JSON serialization of test failures.

Comparison with Other Libraries

Feature Protest proptest quickcheck
Ergonomic API โœ… โŒ โŒ
Automatic Inference โœ… โŒ Partial
Derive Macros โœ… โœ… โœ…
Async Support โœ… โŒ โŒ
Declarative Macros โœ… โŒ โŒ
Fluent Builders โœ… Partial โŒ
Pattern Helpers โœ… โŒ โŒ
Shrinking โœ… โœ… โœ…
Statistics โœ… Partial โŒ
Failure Persistence โœ… Partial โŒ
Test Corpus โœ… โŒ โŒ

Documentation

Full documentation is available on docs.rs.

Key Modules

  • protest::ergonomic - Ergonomic API (closures, builders, patterns)
  • protest::primitives - Built-in generators (int, string, vec, hashmap, etc.)
  • protest::generator - Generator trait and utilities
  • protest::property - Property trait and execution
  • protest::shrink - Shrinking infrastructure
  • protest::persistence - Failure persistence and replay (optional)
  • protest::config - Configuration types
  • protest::statistics - Coverage and statistics

Protest Extras

The protest-extras crate provides 23 additional specialized generators and enhanced shrinking strategies:

See the protest-extras README for detailed examples and documentation.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

Inspired by:


Made with โค๏ธ for the Rust community