proptest
repository·main·Indexed 24 days ago
https://github.com/proptest-rs/proptestA property testing framework for Rust inspired by Python's Hypothesis. It allows developers to verify code properties using automatic input generation and shrinking to find minimal failing cases. The ecosystem includes the proptest-derive crate for automatically implementing the Arbitrary trait and proptest-state-machine for sequential state machine testing.
What's inside proptest
- Proptest is a property-based testing framework for Rust. Unlike traditional unit testing where you provide specific inputs and expected outputs, property-based testing allows you to define general properties that should hold true for a wide range of automatically generated inputs. This helps uncover edge cases and bugs that manual test cases might miss.
Overview of the `proptest` crate
mainTheproptestcrate is the primary entry point for using Proptest. It contains the core testing framework and the majority of available strategies used to generate test data. For detailed API signatures and technical specifications, refer to the official rustdoc documentation.Use proptest-state-machine for sequential state machine testing
mainTheproptest-state-machinecrate provides a strategy and a convenience runner macro designed for testing sequential state machines. It allows you to define a state machine and then automatically generate sequences of actions to verify that the machine maintains its invariants across various state transitions. For detailed implementation details and advanced usage, refer to the official Proptest book.What is Proptest and how does it work?
mainProptest is a property testing framework for Rust inspired by Python's Hypothesis. It tests that specific properties of your code hold true for a wide range of automatically generated arbitrary inputs.
Key features include:
- Automatic Input Generation: Instead of hand-picking inputs, Proptest generates them for you.
- Automatic Shrinking: When a failure is detected, Proptest automatically reduces the failing input to the smallest possible minimal test case that still reproduces the bug.
- Per-value Generation: Unlike QuickCheck, generation and shrinking are defined on a per-value basis rather than per-type, allowing for more flexible and composable test definitions.
What is property testing?
mainProperty testing is a testing methodology where you check that certain properties of your code's output or behavior hold true for a wide range of automatically generated inputs.
Key features include:
- Automatic Input Generation: The framework generates arbitrary inputs for your functions.
- Shrinking: When a failure is found, proptest automatically reduces the input to the smallest possible "minimal failing input" that still reproduces the bug.
- Complementary to Unit Testing: While unit tests check specific known edge cases, property tests search for complex, unexpected inputs that cause failures.
How Proptest differs from QuickCheck
mainProptest and QuickCheck both use random input generation and automatic shrinking to find failing test cases. However, Proptest uses explicit
Strategyobjects instead of relying on type-based generation.Key advantages of Proptest's
Strategyapproach:- Multiple Strategies per Type: You can define many different strategies for a single type (e.g., different integer ranges) without needing newtypes.
- Direct Constraint Expression: You can express ranges directly (e.g.,
0..100) rather than relying on a global size configuration. - Composability: You can compose complex types by creating tuples of components and using
prop_mapto transform them into the target struct. Shrinking is handled automatically via the input types. - Constraint Awareness: Strategies are aware of constraints, reducing the number of rejected inputs during generation and shrinking.
Trade-off: Generating complex values in Proptest can be slower than QuickCheck because Proptest maintains intermediate states to support a richer, integrated shrinking model.
Disadvantages and limitations of filtering
mainFiltering via rejection sampling has several significant drawbacks:
- Performance: It slows down generation because values must be generated multiple times until they satisfy the filter. If a filter always returns
false, the test may never complete. - Rejection Limits: Proptest tracks local and global rejections and aborts if they exceed certain thresholds. By default, proptest allows a large number of local rejections but a relatively small number of global rejections. If your filter is too restrictive, the test will abort.
- Shrinking Interference: Filtering and shrinking do not work well together. If a value is rejected during the shrinking process, proptest treats it as a signal to stop simplifying (calling
complicate()) rather than trying other simpler values that might satisfy the filter.
- Performance: It slows down generation because values must be generated multiple times until they satisfy the filter. If a filter always returns
Understand how Proptest handles failure persistence
mainWhen Proptest identifies a failing test case, it automatically persists that case to a file. This ensures that subsequent test runs replay the known failing input before attempting to generate new random cases. This prevents 'flaky' test behavior where a test fails in one run but passes in the next due to different random seeds.
Key behaviors:
- Storage Location: By default, files are stored in a directory tree rooted at
proptest-regressions. The file name is derived from the source file containing the failing test. - Replay Mechanism: Proptest replays these persisted cases at the start of every test run.
- Implementation Detail: Proptest persists the seed used to generate the failing value rather than the value itself. This makes the persistence more robust to changes in the strategy, as the seed will always produce a valid value within the strategy's domain, even if the strategy has been slightly modified.
- Storage Location: By default, files are stored in a directory tree rooted at
Compare Proptest state machine testing to eqc_statem
mainProptest's state machine testing is inspired by Erlang's
eqc_statem, but has the following key differences:- Strategy Support: Currently, Proptest only supports a
sequentialstrategy (a concurrent strategy is planned for the future). - Variable Types: Proptest does not use "symbolic" variables. The state of the abstract (reference) state machine is kept separate from the state of the system under test (SUT).
- Post-condition Definition: Post-conditions are not defined in a standalone function; they are integrated into the
StateMachineTest::applyfunction.
- Strategy Support: Currently, Proptest only supports a
Compose multiple inputs using Compound Strategies
mainSince
TestRunner::runaccepts only a singleStrategy, you can test functions with multiple arguments by using compound strategies. The most common method is to combine multiple strategies into a single tuple strategy. This produces a single value (a tuple) that contains all the required inputs, which you can then destructure within your test closure.Common types of compound strategies include:
- Tuples: A tuple of strategies (e.g.,
(strategy_a, strategy_b)) is a strategy for tuples of the values those strategies produce. - Fixed-size arrays: Arrays of strategies.
- Vecs:
Vecs of strategies, which produce collections of values parallel to the strategy collection. - Collection module strategies: Various other strategies provided in the
proptest::collectionmodule.
# extern crate proptest; use proptest::test_runner::TestRunner; fn add(a: i32, b: i32) -> i32 { a + b } #[test] # fn dummy() {} // Doctests don't build `#[test]` functions, so we need this fn test_add() { let mut runner = TestRunner::default(); // Combine our two inputs into a strategy for one tuple. Our test // function then destructures the generated tuples back into separate // `a` and `b` variables to be passed in to `add()`. runner.run(&(0..1000i32, 0..1000i32), |(a, b)| { let sum = add(a, b); assert!(sum >= a); assert!(sum >= b); Ok(()) }).unwrap(); } # fn main() { test_add(); }- Tuples: A tuple of strategies (e.g.,
Understand the limitations of property testing
mainProperty testing explores a randomly sampled portion of the input space rather than the entire space. Because time is finite, it is extremely unlikely to find single-value edge cases in large input spaces (e.g., finding a specific value out of $2^{64}$ possibilities).
Key Takeaways:
- Edge Cases: Property testing may miss specific, rare edge cases that exist in large domains.
- Complementary Testing: Traditional unit testing with intelligently selected, manual test cases is still necessary to cover critical edge cases that random sampling might miss.
- Strategy Design: The effectiveness of property testing depends on your
Strategy. A strategy that is too broad (e.g.,.{1,4096}for a parser) might produce inputs that never reach deep logic (like a code generator), making the tests ineffective.
# extern crate proptest; use proptest::prelude::*; proptest! { #[test] # fn dummy(0..1) {} fn i64_abs_is_never_negative(a: i64) { // This actually fails if a == i64::MIN, but randomly picking one // specific value out of 2⁶⁴ is overwhelmingly unlikely. assert!(a.abs() >= 0); } } # fn main() { i64_abs_is_never_negative() }Understand shrinking with ValueTree
mainShrinking is the process of finding the simplest input that causes a test failure. In
proptest, this is managed by theValueTreetype. AValueTreerepresents a tree of possible values generated by a strategy. It provides three key methods for navigating the input space:current(): Returns the value at the current node in the tree.simplify(): Attempts to move to a "simpler" node in the tree. It returnstrueif a simpler value was found, andfalseif no further simplification is possible.complicate(): Attempts to move to a more "complex" node. This is useful when a simplification step results in a value that no longer triggers the failure.
Shrinking is constrained by the strategy's definition. For example, an integer strategy defined as
100..1000i32will shrink towards zero but will stop at100because it cannot exit the defined range.