Example-based tests check the cases you thought of. Property-based testing (PBT) checks thousands of cases you did not — but only if you can articulate the invariants, which is the part most teams find hard.
Tier: Advanced · Workflow area: Testing & Quality
AI is unusually good at exactly that step: reading a spec or a function and proposing candidate properties ("output is always sorted," "encode∘decode is identity," "total is preserved across splits"). You judge the invariants; Hypothesis or fast-check does the hunting; the shrinker hands you a minimal counterexample.
decode(encode(x)) == x).Skip for thin CRUD glue and I/O orchestration — there is no invariant to state, and PBT adds runtime for nothing.
Elicit candidate invariants. Prompt with the spec, not just the code:
Here is the spec for split_invoice(invoice, ratios) and its implementation.
Propose 6-10 candidate properties that must hold for ALL valid inputs.
For each: state the property in one sentence, classify it
(round-trip / invariant / conservation / oracle / metamorphic), and flag
any property that the implementation might currently violate.
Do not propose properties that merely restate the implementation line by line.
Cull to the strong ones. Keep properties that would catch a real bug class; discard tautologies ("returns a list") and spec-restatements. Three sharp properties beat ten weak ones. This judgment step is yours — a wrong property becomes a permanent false alarm.
Generate the harness. Ask for the test with explicit generator constraints:
Write Hypothesis tests for the 3 approved properties. Constraints:
- ratios: lists of 1-10 Decimals in (0,1] that sum to 1 (build a composite strategy).
- invoice amounts: Decimal, 2 places, 0.01 to 10_000_000.
- Property 1 (conservation): sum of split amounts == invoice.total exactly.
- Property 3 (metamorphic): permuting ratios permutes outputs identically.
- Use @settings(max_examples=500) and add an explicit @example for the
known edge: single ratio of exactly 1.
Run and read the shrunk counterexample. When a property fails, Hypothesis shrinks to a minimal case — e.g. ratios=[Decimal('0.333...'), ...] exposing a rounding-remainder bug. Paste the counterexample back to the AI for a fix proposal, but decide yourself whether the code or the property was wrong. Both happen.
Pin regressions. Every real counterexample becomes a permanent @example(...) so the exact case replays forever, independent of random generation.
For migrations, the killer property is the oracle: run old and new implementations on the same generated inputs and assert equality. AI writes this harness in minutes, and it routinely finds divergences code review missed.
+ to -); a property that never fails verifies nothing.@example.max_examples; 100–500 covers most CI budgets).assume(sum(ratios) == 1) throws away almost every generated input; build a strategy that constructs valid ratios directly.Invariant elicitation works on redacted material: paste the spec with product nouns neutralized ("ledger entry" → "record") and the function signature only. Local Qwen-Coder-class models via Ollama produce serviceable Hypothesis harness code from an explicit property list, though invariant brainstorming quality drops — compensate by mining properties from the spec's own "must/always/never" sentences yourself and using the local model just for strategy code. Everything else (running, shrinking, pinning) is fully local anyway.
@example pins exist for every counterexample found.