Defining Truths, Not Examples
Think of a property like a law of physics for your function. Gravity always pulls things down — you don't need to test it with every possible object to know it applies. In Property-Based Testing, you define properties: characteristics of your function that must hold true for any valid input, not just the ones you hand-picked.
Consider a function that reverses a list of numbers.
Example-Based approach:
Input: [1, 2, 3] → Expected output: [3, 2, 1]
This test passes. But it only proves the function works for this one specific list.
Property-Based approach:
Ask yourself: what is always true about reversing a list, regardless of its contents?
- Size invariant:
len(reverse(xs)) == len(xs)— reversing never adds or removes elements. - Idempotency under double application:
reverse(reverse(xs)) == xs— reversing twice returns the original. - First/last swap:
reverse(xs)[0] == xs[-1]— the last element becomes the first (for non-empty lists).
The Engine Works for You
Once you define these properties, a PBT library takes over. In Python, the dominant library is Hypothesis. In Go, it is rapid or the built-in testing/quick. In Java, jqwik is widely used.
The library acts as a relentless QA engineer. It generates hundreds or thousands of random inputs — empty lists, single-element lists, lists with duplicate values, lists containing the maximum integer, lists containing negative numbers — and runs your code against every defined property.
Here is what this looks like with Hypothesis in Python:
from hypothesis import given
import hypothesis.strategies as st
def reverse_list(xs):
return xs[::-1]
@given(st.lists(st.integers()))
def test_reverse_is_idempotent(xs):
assert reverse_list(reverse_list(xs)) == xs
@given(st.lists(st.integers()))
def test_reverse_preserves_length(xs):
assert len(reverse_list(xs)) == len(xs)
When you run pytest, Hypothesis silently generates and tests hundreds of random lists. If any input breaks a property, it reports the failure with the exact input that caused it.
Choosing Good Properties
The hardest part of PBT is identifying properties worth testing. Common patterns include:
| Pattern | Description | Example |
|---|---|---|
| Round-trip | Encode then decode returns original | decode(encode(x)) == x |
| Idempotency | Applying twice equals applying once | sort(sort(x)) == sort(x) |
| Invariant | Something never changes | Length preserved after filter |
| Commutativity | Order of inputs doesn't matter | add(a, b) == add(b, a) |
| Oracle | Compare against a known-correct (slow) implementation | Fast sort matches naive sort |
Interviewers at senior levels sometimes ask: "How do you test a function when you don't know the exact expected output?" The answer is Property-Based Testing. Describe the round-trip pattern (encode/decode), the oracle pattern (compare a fast implementation against a slow but obviously correct one), and idempotency. These demonstrate you think about correctness as a set of invariants, not just a lookup table of expected values.