4.5.17 · D3Software Engineering

Worked examples — Property-based testing

3,971 words18 min readBack to topic

This page is the hands-on companion to Property-based testing. The parent note taught you what properties are (the RIIOM patterns) and why shrinking matters. Here we walk every case class a property test can face — one input class per worked example — so you never meet a scenario you have not already seen solved.

Everything here is written from zero. If a word looks new, it will be built before it is used.


The scenario matrix

Below is the full grid this topic can throw at you. Each row is a cell — a distinct kind of input or twist. Every cell is claimed by at least one worked example further down.

Cell What it stresses Example that covers it
C1 · Normal middle Property holds for typical inputs Ex 1 (reverse round-trip)
C2 · Empty input [], "", size 0 — division / min / max traps Ex 2 (average crash)
C3 · Singleton Exactly one element — many properties trivially true Ex 3 (sort idempotence)
C4 · Sign trap Negatives, negative zero, mixed signs Ex 4 (abs round-trip)
C5 · Degenerate shape All-equal / already-sorted / duplicates Ex 5 (sort invariant)
C6 · Boundary / limit Largest, smallest, float edges (inf, NaN, subnormal) Ex 6 (float round-trip)
C7 · Oracle disagreement Fast vs slow reference diverge Ex 7 (custom vs builtin sort)
C8 · Real-world word problem A property hides inside a story Ex 8 (shopping cart total)
C9 · Exam-style twist "Why did shrinking stop here?" reasoning Ex 9 (shrink trace)
C10 · Non-termination trap Generator/assume filters too much Ex 10 (assume overuse)

Example 1 — Normal middle (C1, RIIOM: Round-trip)

This is the "middle" cell: nothing weird, property just holds. The value of PBT is that the next nine examples are the inputs you would never have typed by hand.


Example 2 — Empty input (C2, RIIOM: Invariant)


Example 3 — Singleton (C3, RIIOM: Idempotence)

Cell lesson: always ask "is my property trivially true for tiny inputs?" If yes, your confidence must come from the larger generated cases.


Example 4 — Sign trap (C4, RIIOM: Round-trip fails → Invariant)


Example 5 — Degenerate shape (C5, RIIOM: Invariant)

Figure — Property-based testing
Figure s01 — C5, sort length invariant. Blueprint bar chart. The horizontal axis lists four degenerate input lists ([5,5,5], [1,2,3], [3,2,1], [2,2,1]); the vertical axis is list length (element count), gridded 0–4. For each input a cyan bar shows the input length and an amber bar shows the output length of a buggy dedupe-then-sort. The first three inputs stay at length 3 (correct). The fourth, [2,2,1], drops from 3 to 2 — the amber arrow points at this failing cell and reads "invariant FAILS, len 3 → 2". A legend names the two bar colours; the small text over each bar shows the actual list produced.


Example 6 — Boundary / float limit (C6, RIIOM: Round-trip)


Example 7 — Oracle disagreement (C7, RIIOM: Oracle)


Example 8 — Real-world word problem (C8, RIIOM: Invariant)

Cell lesson: business rules ("total can't be negative") are properties. This is the everyday face of Invariants and contracts.


Example 9 — Exam-style twist: why did shrinking stop? (C9, RIIOM: Invariant)

Figure — Property-based testing
Figure s02 — C9, shrinking staircase. Blueprint step plot. The horizontal axis is the shrink step (greedy descent, 0–4); the vertical axis is input complexity measured as element count (0–6, gridded). The cyan staircase starts at the first random failure [847,-3,901,0,-12] (complexity 5) and drops as the framework removes elements while the failure persists, reaching [-3] then [-1] (complexity 1). The amber arrow marks the moment the framework tries [0], which passes ("not negative"), so that candidate is rejected and the descent stops at the minimum [-1].

Recall Why greedy descent gives a

minimal (not necessarily global minimal) case Shrinking is local: it stops at the first point where every neighbouring simplification passes. This is a local minimum of "complexity subject to still failing" — usually tiny and clear, which is all a debugger needs. What does shrinking minimise? ::: Input complexity, subject to the constraint that the input still fails the property.


Example 10 — Non-termination trap (C10, RIIOM: any pattern, generator side)


Putting the matrix back together

Recall Self-test: which example covered which cell?

Empty input crash ::: Example 2 (C2) Singleton makes idempotence trivially true ::: Example 3 (C3) Sign loss means no round-trip, use an invariant ::: Example 4 (C4) All-equal / duplicates stress the sort invariant ::: Example 5 (C5) Float NaN / infinity break equality round-trip ::: Example 6 (C6) Trusted oracle exposes a wrong sort key ::: Example 7 (C7) Real-world business rule as an invariant ::: Example 8 (C8) Shrinking stops at a local minimum ::: Example 9 (C9) Over-filtering with assume stalls the run ::: Example 10 (C10)

Every cell of The scenario matrix now has a worked, verified example. When you write your own property tests, run this checklist: middle, empty, one, sign, shape, edge, oracle, story, shrink, filter.


Connections

  • Property-based testing — this is its worked-examples child page.
  • Unit testing — Examples 2 and 6 show where a hand-written unit test complements a property.
  • Invariants and contracts — Examples 2 and 8 are contract decisions in disguise.
  • Hypothesis (Python)assume, min_size, and custom strategies used in Ex 2 and 10.
  • QuickCheck (Haskell) — the original tool that popularised shrinking (Ex 9).
  • Fuzzing — Example 10 contrasts unstructured generation with valid generators.
  • Pure functions — every property here assumes deterministic, side-effect-free code.