Property-Based Testing

Example-based tests check the inputs you thought of; the bugs live in the inputs you didn't. Property-based testing (PBT) inverts the contract: you state something that must hold for all valid inputs, and the framework generates hundreds of them — adversarially biased toward the nasty ones — hunting for a counterexample. Born as QuickCheck in Haskell, the technique is mainstream now via Hypothesis (Python), jqwik (JVM), and fast-check (JavaScript/TypeScript). It routinely finds boundary bugs — empty inputs, Unicode astral-plane strings, integer edges, NaN — that years of example tests miss.

Properties worth writing

The skill of PBT is finding properties, and a small taxonomy covers most real code:

Hypothesis in Python

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_is_idempotent_permutation(xs):
    out = my_sort(xs)
    assert out == sorted(out)                 # ordered
    assert sorted(out) == sorted(xs)          # same multiset

@given(st.dictionaries(st.text(), st.integers() | st.text() | st.none()))
def test_config_roundtrip(d):
    assert parse_config(render_config(d)) == d

Strategies compose: st.builds(Order, item=st.sampled_from(SKUS), qty=st.integers(1, 999)) generates domain objects; .filter() and .map() refine; st.data() supports dependent draws. Hypothesis persists a local database of past failures and replays them first — a found bug stays found. The @example(...) decorator pins regression cases explicitly.

jqwik and fast-check

The same model translates directly. jqwik rides JUnit 5:

@Property
void roundTrip(@ForAll("orders") Order o) {
    assertThat(Order.parse(o.serialize())).isEqualTo(o);
}
@Provide Arbitrary<Order> orders() {
    return Combinators.combine(Arbitraries.strings().alpha(),
                               Arbitraries.integers().between(1, 999))
                      .as(Order::new);
}

fast-check mirrors it in TypeScript: fc.assert(fc.property(fc.array(fc.integer()), xs => ...)), with first-class async property support for testing promise-based code. Kotlin's kotest and Rust's proptest complete the mainstream coverage — the concepts transfer verbatim.

Shrinking: from random noise to minimal counterexample

When a property fails on a 400-element list, the framework does not hand you that monster — it shrinks, systematically simplifying the input while the failure persists, and reports a minimal case ([0, -1], the empty string, a single NaN). Shrinking is the difference between PBT being usable and being a noise generator, and it is why integrated shrinking (Hypothesis, jqwik, fast-check all have it) matters: the minimal counterexample usually states the bug almost verbatim.

Stateful and model-based testing

Beyond pure functions, PBT generates operation sequences against stateful systems, checking behavior against a simple in-memory model: Hypothesis's RuleBasedStateMachine and fast-check's commands API generate interleavings of put/get/delete/reopen against your cache, store, or state machine, comparing to a dict-backed model. This is a lightweight cousin of full model-checking and it excels at finding sequence-dependent bugs — the delete-then-reinsert-loses-TTL class — that no hand-written scenario covers.

Fitting PBT into a real suite

See Also