proptest

repository·main·Indexed 24 days ago

https://github.com/proptest-rs/proptest

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

Tokens
31.8K
Snippets
77
Records
147
Agent score
81%

What's inside proptest

  1. Introduction to Proptest

    main
    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.
  2. Use proptest-state-machine for sequential state machine testing

    main
    The proptest-state-machine crate 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.
  3. What is Proptest and how does it work?

    main

    Proptest 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.
  4. What is property testing?

    main

    Property 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.
  5. How Proptest differs from QuickCheck

    main

    Proptest and QuickCheck both use random input generation and automatic shrinking to find failing test cases. However, Proptest uses explicit Strategy objects instead of relying on type-based generation.

    Key advantages of Proptest's Strategy approach:

    • 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_map to 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.

  6. Disadvantages and limitations of filtering

    main

    Filtering 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.
  7. Understand how Proptest handles failure persistence

    main

    When 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.
  8. Compare Proptest state machine testing to eqc_statem

    main

    Proptest's state machine testing is inspired by Erlang's eqc_statem, but has the following key differences:

    • Strategy Support: Currently, Proptest only supports a sequential strategy (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::apply function.
  9. Compose multiple inputs using Compound Strategies

    main

    Since TestRunner::run accepts only a single Strategy, 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::collection module.
    # 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(); }
  10. Understand the limitations of property testing

    main

    Property 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() }
  11. Understand shrinking with ValueTree

    main

    Shrinking is the process of finding the simplest input that causes a test failure. In proptest, this is managed by the ValueTree type. A ValueTree represents 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 returns true if a simpler value was found, and false if 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..1000i32 will shrink towards zero but will stop at 100 because it cannot exit the defined range.