Free — no signup required

Property-Based Testing (PBT)

3 min read

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?

  1. Size invariant: len(reverse(xs)) == len(xs) — reversing never adds or removes elements.
  2. Idempotency under double application: reverse(reverse(xs)) == xs — reversing twice returns the original.
  3. 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
Interview Tip

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.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free