Property-Based Testing for Rust - An ergonomic, powerful, and feature-rich property testing library with minimal boilerplate.
- 🚀 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
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 proptestSee individual package READMEs for detailed documentation:
- protest-extras - Additional generators
- protest-stateful - Stateful testing
- protest-criterion - Benchmarking
- protest-insta - Snapshot testing
- protest-proptest-compat - Migration guide
- protest-cli - Command-line tool
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);
}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");
}use protest::property_test;
#[property_test(iterations = 100)]
fn test_string_length(s: String) {
// Generator automatically inferred from type
assert!(s.len() >= 0);
}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);
}Protest offers multiple API styles - use what fits your needs:
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"
);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");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]));
}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());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
);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.
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();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: 50Extensive 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()
};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:
- Failed tests are automatically saved to
.protest/failures/ - On subsequent runs, failures are replayed before running new cases
- Fixed failures are automatically cleaned up
Install the CLI tool for advanced failure management:
cargo install protest-cliSee the CLI documentation for complete details on managing failures, generating regression tests, and corpus building.
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.
The repository includes comprehensive examples:
basic_usage.rs- Getting startedergonomic_api_demo.rs- All ergonomic featurescustom_structs.rs- Custom types with deriveasync_properties.rs- Async testingadvanced_patterns.rs- Advanced techniques
Run examples:
cargo run --example ergonomic_api_demo
cargo run --example custom_structs
cargo run --example async_propertiesBenchmark 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.
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.
Use protest-proptest-compat for migration helpers:
proptest! {
#[test]
fn test_addition(a in 0..100i32, b in 0..100i32) {
assert!(a + b >= a && a + b >= b);
}
}#[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.
[features]
default = ["derive"]
derive = ["protest-derive"] # Derive macros for Generator trait
persistence = ["serde", "serde_json"] # Failure persistence & replayProtest 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.
| 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 | ✅ | ❌ | ❌ |
Full documentation is available on docs.rs.
protest::ergonomic- Ergonomic API (closures, builders, patterns)protest::primitives- Built-in generators (int, string, vec, hashmap, etc.)protest::generator- Generator trait and utilitiesprotest::property- Property trait and executionprotest::shrink- Shrinking infrastructureprotest::persistence- Failure persistence and replay (optional)protest::config- Configuration typesprotest::statistics- Coverage and statistics
The protest-extras crate provides 23 additional specialized generators and enhanced shrinking strategies:
See the protest-extras README for detailed examples and documentation.
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
Inspired by:
- proptest - Rust property testing
- QuickCheck - Original Rust QuickCheck
- Hypothesis - Python property testing
Made with ❤️ for the Rust community