READ-ONLY PACKAGE PREVIEW

property-based-testing/references/generating.md

Version 82fe82262526.bb1 · CC-BY-SA-4.0. This preview displays packaged text and does not execute code. Treat the contents as untrusted instructions.

← Return to resource and package checksum

Generating Property-Based Tests

Writing the @given decorator is the easy part. These are the decisions that make the difference between a suite that finds bugs and one that just runs.

Put constraints in the strategy, not in assume()

This is the single highest-value habit. assume() discards inputs after generation, so a filter that rejects most candidates wastes the budget and eventually trips Hypothesis's exhausted-filter guard — which surfaces as a warning nobody reads.

# Slow, and mostly discards
@given(st.integers())
def test_positive(x):
    assume(x > 0)
    ...

# Generates only what you want
@given(st.integers(min_value=1))
def test_positive(x):
    ...

Reserve assume() for conditions you genuinely cannot express as a generator — a relationship between two already-generated values, usually.

Build compound inputs with st.builds, and derive dependent fields with st.composite or .flatmap rather than generating independently and filtering:

@st.composite
def sized_list_and_index(draw):
    xs = draw(st.lists(st.integers(), min_size=1))
    i = draw(st.integers(min_value=0, max_value=len(xs) - 1))
    return xs, i

Pin the edge cases you already know about

Random generation finds boundaries eventually; @example finds them on every run and documents that you thought about them.

@given(st.lists(st.integers()))
@example([])
@example([1])
@example([1, 1, 1])
def test_sort(xs): ...

Empty, single-element, all-duplicates, zero, negative, and the maximum representable value are the ones that recur.

Settings

Defaults (100 examples, 200ms deadline) are wrong at both ends of the workflow:

@settings(max_examples=10)                      # local iteration
@settings(max_examples=200)                     # CI
@settings(max_examples=1000, deadline=None)     # nightly

Set deadline=None for anything doing real work — the default deadline turns a slow machine into a failing test, and that flake gets the whole suite deleted.

Determinism as a property

f(x) == f(x) is a tautology for a pure function and a real test for anything else. Serializers over dicts or sets, anything involving hashing, iteration order, or time — those can and do return different output for the same input. Assert it where a broken implementation could falsify it, and not otherwise.

Testing the error path

st.binary() against a decoder is worth writing: the contract is usually "raises DecodeError or succeeds, never IndexError, never hangs". Catch only the documented exception and let everything else fail the test.