property-testing

Property-based and generative testing across the polyglot stack. TRIGGER when: user asks about property-based testing, generative testing, QuickCheck, Hypothesis, proptest, StreamData, fast-check, fuzzing test inputs, or finding edge cases that example tests miss. DO NOT TRIGGER when: user asks about TDD workflow (use tdd), mutation testing (use tdd), load testing (use performance-profiler), or security fuzzing (use security-audit).

You are a Senior Test Architect -- you distrust example-based tests that only prove the author's assumptions, and you design properties that let the machine find the bugs you didn't imagine.

property-testing

Property-based testing (PBT) generates random inputs, checks that properties hold for all of them, and shrinks failures to minimal counterexamples. It finds edge cases that hand-written examples miss.

What You Get

  • Property definitions for the target code (roundtrip, invariant, oracle, metamorphic)
  • Custom generators tailored to the domain, not just random noise
  • Deterministic CI configuration (fixed seeds, bounded examples)
  • Shrunk counterexamples with reproduction commands

When PBT Beats Example Tests

Use PBTStick with examples
Parsers, serializers (roundtrip property)Simple CRUD operations
Sorting, filtering (invariant properties)UI rendering
State machines, protocols (model-based)Integration tests with external services
Numeric algorithms (oracle: compare to reference)Code where the property restates the implementation
Any function with a large input spaceGlue code with minimal logic

The litmus test: Can you state what should be true for ALL inputs without restating the implementation? If yes, use PBT. If the property is just assert f(x) == implementation(x), you are testing nothing.

Workflow

1. Identify properties

Before writing any generators, identify what properties the code should satisfy. See properties.md for the full taxonomy. Common starting points:

  • Roundtrip: decode(encode(x)) == x
  • Invariant: len(sort(xs)) == len(xs)
  • Idempotent: f(f(x)) == f(x)
  • Commutative: f(a, b) == f(b, a) (when applicable)

2. Build generators

Use the language's built-in generators first. Only build custom generators when the input domain has constraints (e.g., valid email addresses, balanced trees). See generators.md.

3. Write the property test

# Python (Hypothesis)
from hypothesis import given, settings
import hypothesis.strategies as st

@given(st.lists(st.integers()))
@settings(max_examples=200, derandomize=True)
def test_sort_preserves_length(xs):
    assert len(sorted(xs)) == len(xs)
// Rust (proptest)
proptest! {
    #[test]
    fn sort_preserves_length(xs: Vec<i32>) {
        let sorted = sort(&xs);
        prop_assert_eq!(sorted.len(), xs.len());
    }
}
# Elixir (StreamData)
property "sort preserves length" do
  check all xs <- list_of(integer()) do
    assert length(Enum.sort(xs)) == length(xs)
  end
end

4. Run, shrink, fix

When a property fails, the framework shrinks the input to the minimal counterexample. See shrinking.md for debugging strategies.

5. Lock the seed for CI

Every PBT run must be reproducible. Set a fixed seed and bounded example count in CI. Allow unbounded exploration in local development.

Tools by Language

LanguageLibraryInstallSeed flag
PythonHypothesispip install hypothesis@settings(derandomize=True)
Rustproptestproptest = "1" in Cargo.tomlPROPTEST_SEED=<n> env var
ElixirStreamData{:stream_data, "~> 1.0", only: :test}--seed <n> in mix test
TypeScriptfast-checknpm i -D fast-checkfc.assert(prop, { seed })
Gogoptergo get github.com/leanovate/goptergopter.DefaultGenParameters().Seed(n)
HaskellQuickCheckcabal install QuickCheckquickCheckWith stdArgs{replay=Just(seed,0)}

Relationship to TDD

PBT fits into the TDD workflow as a validation step after the incremental loop:

Phase 1: Planning
Phase 2: Tracer Bullet (one example test)
Phase 3: Incremental Loop (example tests, RED-GREEN)
Phase 3.5: Property Testing  <-- YOU ARE HERE
Phase 4: Mutation Testing (see tdd skill)
Phase 5: Refactor

PBT and mutation testing target different gaps:

  • PBT: "does the code handle inputs I didn't think of?"
  • Mutation testing: "would my tests catch a bug if one were introduced?"

Common Pitfalls

MistakeFix
Property just restates the implementationFind a different angle: roundtrip, invariant, comparison to reference
Generator produces invalid domain inputsAdd assume() / filter() or build a custom generator
Tests are slow (>30s)Reduce max_examples, use @settings(deadline=None) only locally
Flaky CI from non-deterministic seedsAlways set derandomize=True or equivalent in CI
Only testing pure functionsUse model-based testing for stateful systems

Reading guide

TopicFile
Property taxonomy with examplesproperties.md
Custom generator patternsgenerators.md
Shrinking and debugging failuresshrinking.md

See also

  • tdd -- red-green-refactor workflow; PBT extends Phase 3
  • tdd (mutation-testing sub-file) -- complementary validation (tests the tests, not the input space)