DEV Community

Donald Johnson
Donald Johnson

Posted on

Confidence Through Diversity: A Deep Meta-Analysis of Property Testing in DivvySim

Repository: divvysim


Property testing has revolutionized my approach to quality assurance in modern software development. Instead of writing fixed input–output examples, I declare the invariants my system must uphold for every possible input. In the DivvySim project, this approach has been instrumental in verifying that complex monetary calculations—such as equitably splitting an amount among multiple recipients—are both accurate and robust.

In this article, I detail my meta-level strategy and share real examples from DivvySim. My goal is to provide fellow testers and researchers with both the conceptual framework and concrete code examples needed to integrate property testing into their own projects.


A Meta-Approach to Property Testing

Defining Invariants

At the core of property testing is the identification of invariants—properties that must always hold true regardless of the input. In DivvySim, I established several key invariants:

  • Total Conservation: The sum of all distributed shares must equal the original monetary amount.
  • Precision Consistency: Each computed share must adhere to the desired decimal precision (or “scale”), ensuring that rounding is handled correctly.
  • Robust Conversion: Conversions from a decimal representation to an integer-based internal representation (to avoid floating-point imprecision) must be accurate.

Detailed Metadata Collection

Every property test in DivvySim is designed to capture detailed metadata so that I can trace issues back to their source:

  • Input Parameters: Each test logs the monetary amount, number of recipients, and scale.
  • Random Seeds and Shrink History: In the event of failure, the framework automatically shrinks inputs to the minimal failing case and records the random seed, ensuring that errors can be reproduced precisely.
  • Step-by-Step Traces: Logs are recorded at each transformation—from the raw input to the final output—to diagnose any deviations.

Advanced Input Generation

My property tests rely on multiple techniques for input generation:

  • Range Testing: I generate inputs that span from tiny fractional amounts to extremely large values.
  • Fuzzing: Randomized input generation helps uncover edge cases that might otherwise be missed.
  • Domain-Specific Generators: For financial applications like DivvySim, I use generators that mimic real-world monetary values with precise scaling.

Detailed Examples from DivvySim

Below are some concrete examples drawn directly from DivvySim. These code snippets illustrate how I verify the invariants and log metadata for each test.

Example 1: Total Conservation Test

This test verifies that the sum of the distributed shares equals the original amount.

#[test]
fn test_expected_total() {
    // Parse a precise decimal amount (e.g., "100.01")
    let amount = Decimal::from_str("100.01").unwrap();
    let recipients = 4;
    let scale = 2;

    // Generate the shares based on the given amount, number of recipients, and scale
    let splits = split_decimal(amount, recipients, scale);

    // Compute the total of all splits
    let total: Decimal = splits.iter().copied().sum();

    // Assert that the sum of the splits equals the original amount
    assert_eq!(total, amount, "Total of splits must equal the input amount");
}
Enter fullscreen mode Exit fullscreen mode

Example 2: Precision and Conversion Test

This test ensures that converting a decimal to its internal integer representation maintains the correct scale.

#[test]
fn test_decimal_conversion() {
    // The input amount with defined precision
    let amount = Decimal::from_str("123.45").unwrap();
    let recipients = 7;
    let scale = 2;

    // Convert the amount to an integer-based representation
    let amount_int = amount.trunc().to_i128().unwrap_or(0);
    let base_share = Decimal::from_i128_with_scale(amount_int / (recipients as i128), scale);

    // Log metadata for reproducibility
    println!("Input: {}, Converted: {}, Base share: {}", amount, amount_int, base_share);

    // Check that the conversion produces a non-negative base share
    assert!(base_share >= Decimal::from_i128_with_scale(1, scale));
}
Enter fullscreen mode Exit fullscreen mode

Example 3: Shrinkage of Failing Inputs

When a test fails, the framework “shrinks” the input to its minimal failing case. For example, if a property test checking the invariant of minimal share distribution fails, the framework captures the simplest failing input:

#[test]
fn test_reset_exceed_minimal_units() {
    // An amount that might lead to a distribution where some recipients receive zero shares
    let amount = Decimal::from_str("12.34567889").unwrap();
    let recipients = 5;
    let scale = 2;

    // Generate the shares
    let splits = split_decimal(amount, recipients, scale);

    // Sum the splits to verify total conservation
    let total: Decimal = splits.iter().copied().sum();

    // Assert that the total equals the input amount
    assert_eq!(total, amount, "Total should match the input amount even after shrinkage");

    // In a failing case, the minimal input will be logged automatically for analysis
}
Enter fullscreen mode Exit fullscreen mode

Example 4: Extreme Value Testing

I also test the boundaries by running property tests with extremely large monetary values.

#[test]
fn test_extremely_large_values() {
    // Test with an extremely large amount to stress the conversion and precision
    let amount = Decimal::from_str("999999999999999999.99").unwrap();
    let recipients = 10;
    let scale = 2;

    // Generate the splits
    let splits = split_decimal(amount, recipients, scale);
    let total: Decimal = splits.iter().copied().sum();

    // Log details for analysis
    println!("Testing extremely large value: {}", amount);
    println!("Total computed from splits: {}", total);

    // Assert that the total remains equal to the original amount
    assert_eq!(total, amount, "Even for extremely large amounts, conservation must hold");
}
Enter fullscreen mode Exit fullscreen mode

Best Practices and Testing Vectors

When I build property tests, I follow these best practices to ensure thorough coverage and reproducibility:

Comprehensive Range Coverage

  • Numeric and Boundary Values: I generate tests that span the entire range—from minimal fractional values to astronomical figures.
  • Edge Conditions: I always include boundary values to capture off-by-one and rounding errors.

Automated Randomization and Fuzzing

  • Random Input Generation: Fuzzing allows me to generate diverse test cases that expose hidden edge cases.
  • Reproducibility: I capture random seeds and shrink histories so that any failure can be reproduced with pinpoint accuracy.

Domain-Specific Input Generators

  • Tailored Data: I develop generators that reflect real-world monetary scenarios, ensuring that my tests simulate practical usage.
  • Automatic Shrinkage: Implementing shrinkers minimizes failing test cases, which greatly simplifies debugging.

Detailed Metadata Logging

  • Input Context: I log every test’s input parameters, conversions, and computed results.
  • Step-by-Step Traces: Detailed logs of each transformation help diagnose deviations from expected behavior.

Conclusion

Property testing has given me a profound level of confidence by ensuring that critical invariants hold for every possible input. In DivvySim, my approach to property testing—encompassing invariant definition, robust metadata logging, and sophisticated input generation—has proven invaluable. The detailed strategies and examples presented here serve as a roadmap for any tester or researcher aiming to build resilient, reliable software.

For further details and to explore the full source code, please visit the DivvySim repository.

Embrace the power of property testing with confidence, and let the diversity of your tests and the depth of your meta-analysis elevate your quality assurance practices.

Top comments (0)