Skip to main content

Property-Based Testing

Property-based testing states general properties and lets a framework generate many randomized inputs to find and shrink counterexamples. It uncovers edge cases that example-based tests miss, strengthening confidence during change and migration.

Best Practice: Property-Based Testing

Property-based testing, pioneered by the QuickCheck library from Koen Claessen and John Hughes, asks you to state general properties your code must satisfy rather than hand-picking individual examples. A framework then generates hundreds or thousands of randomized inputs and checks that each property holds. When it finds a failing case, it "shrinks" the input to a minimal counterexample that is easy to debug. This matters because example-based tests only cover the cases a human thought of, while property-based tests probe the input space systematically and routinely uncover edge cases around boundaries, empty values, and unusual encodings.

Step-by-Step Implementation Guidance

  1. Identify properties that should always hold, such as round-trip (decode of encode equals input) or invariants.
  2. Choose a property-based framework for your language.
  3. Define generators that produce valid inputs for the code under test.
  4. Write the property as a function of generated inputs returning a boolean assertion.
  5. Run with a high example count locally and in CI.
  6. When a failure is found, use the shrunk counterexample to fix the bug, then add it as a regression test.
  7. Combine property tests with targeted example tests for known critical cases.

Common Mistakes Teams Make When Ignoring This Practice

  • Relying solely on a handful of happy-path examples.
  • Writing properties that merely restate the implementation.
  • Using generators that never produce edge cases like empty or maximal values.
  • Setting the example count so low that rare bugs slip through.
  • Ignoring shrunk counterexamples instead of turning them into fixed regressions.

Tools and Techniques That Support This Practice

  • Frameworks: QuickCheck (Haskell), Hypothesis (Python), fast-check (JavaScript), jqwik (Java), PropEr (Erlang).
  • Custom generators and combinators for domain types.
  • Shrinking to minimize counterexamples.
  • Stateful/model-based testing for sequences of operations.
  • CI integration with seeded runs for reproducibility.

How This Practice Applies to Different Migration Types

  • Cloud Migration: Use properties to confirm serialization and idempotency hold across new infrastructure.
  • Database Migration: Assert round-trip and invariant properties to validate data mapping on the target engine.
  • SaaS Migration: Generate varied payloads to confirm an adapter handles the new provider's full input range.
  • Codebase Migration: Run the same properties against old and new implementations to verify equivalence.

Checklist

  • Key invariants and round-trip properties are identified.
  • A property-based framework is configured for the language.
  • Generators cover boundary and edge-case inputs.
  • Properties assert behavior, not the implementation.
  • Runs use a high example count in CI.
  • Failing counterexamples are added as regression tests.
  • Seeds are recorded so failures are reproducible.