An Introduction to Property-Based Testing

Traditional unit tests check specific examples; property-based testing instead checks that a general property holds true across hundreds of randomly generated inputs — genuinely good at finding edge cases a human simply wouldn’t think to write by hand.

from hypothesis import given, strategies as st

# Traditional example-based test: checks ONE specific case
def test_reverse_specific_case():
    assert list(reversed([1, 2, 3])) == [3, 2, 1]

# Property-based test: checks a GENERAL property across MANY generated inputs
@given(st.lists(st.integers()))
def test_reverse_twice_returns_original(lst):
    # This property should hold for EVERY possible list, not just one example
    assert list(reversed(list(reversed(lst)))) == lst

@given(st.lists(st.integers()))
def test_sorted_list_is_actually_ordered(lst):
    sorted_lst = sorted(lst)
    for i in range(len(sorted_lst) - 1):
        assert sorted_lst[i] <= sorted_lst[i + 1]

Why This Finds Bugs Example-Based Tests Miss

Hypothesis (Python’s property-based testing library, with equivalents in most languages) generates hundreds of varied inputs automatically — including deliberately nasty edge cases like empty lists, single-element lists, very large numbers, and duplicate values — and when it finds a failing case, it automatically “shrinks” it down to the smallest possible input that still reproduces the failure, making the actual bug far easier to diagnose than a randomly generated large failing example would be.

Where It’s Worth the Setup

Property-based testing shines for pure functions with a genuine mathematical property to verify (sorting, serialization round-trips, mathematical invariants) — it’s real additional setup effort, so it’s worth reserving for code where edge cases are both likely and genuinely hard to enumerate by hand.