Overview
Property-based testing (PBT) replaces hand-picked examples with a property — a claim
that must hold for all valid inputs — and lets the framework generate hundreds of cases,
including the pathological ones nobody types by hand (empty strings, Integer.MIN_VALUE,
surrogate pairs). jqwik is the modern Java choice: a native JUnit 5 Platform engine, so
properties run alongside Jupiter tests in the same build and IDE. QuickTheories and
junit-quickcheck preceded it and still appear in older codebases, but jqwik is where the
ecosystem settled.
Key points
@Property+@ForAll: a property method takes generated parameters and returnsboolean/uses assertions; jqwik runs it 1000 times by default with varied inputs.- Generators: built-in
Arbitrariesfor primitives, strings, collections; constrain via annotations (@IntRange,@AlphaChars,@Size) or compose programmatically with@Providemethods (map,flatMap,filter,Combinators.combine). - Shrinking is the killer feature: on failure, jqwik searches for a minimal failing
input — you debug
""or0, not a 40-character random string. - Reproducibility: failures report a seed; jqwik records it (
.jqwik-database) and re-runs the failing sample first on the next run. - Stateful/model-based testing: generate sequences of actions against the system with a simplified model as oracle; jqwik's action-chain support shrinks the failing sequence itself — superb for stateful components (caches, state machines).
- PBT complements, not replaces, examples: keep example tests as documentation and regression pins; add properties where the input space is large and an invariant exists.
- One-liners on the alternatives: QuickTheories — JUnit-agnostic, fluent-API PBT library, low activity; junit-quickcheck — annotation-driven PBT for JUnit 4, legacy.
Details
Finding properties
- Invariants: outputs that always hold — a sort's output is ordered and a permutation of the input; a balance never goes negative.
- Round-trips:
decode(encode(x)) == x— serialisation, parsing, encryption, normalisation. The single highest-yield pattern. - Oracles: compare the optimised implementation against a slow-but-obvious reference implementation on random inputs.
- Metamorphic relations: without knowing the exact output, relate outputs of related
inputs —
f(x + y) == f(x) + f(y), adding an item never lowers a total. - Idempotence:
normalize(normalize(x)) == normalize(x).
When PBT beats examples
- Parsers, codecs, formatters — huge input spaces, crisp round-trip properties.
- Arithmetic on money/time/units — overflow and boundary behaviour.
- Custom data structures and concurrency-adjacent state machines (via action chains).
- Anywhere a bug report starts with "only fails for input …" — the class of bug examples systematically miss.
Less suited: glue code with trivial input spaces, and code whose "property" would just restate the implementation.
Examples
class MoneyProperties {
@Property
void parsingIsTheInverseOfFormatting(@ForAll("money") Money m) {
assertThat(Money.parse(m.format())).isEqualTo(m);
}
@Property
void additionNeverLosesCents(@ForAll("money") Money a, @ForAll("money") Money b) {
assertThat(a.plus(b).cents()).isEqualTo(a.cents() + b.cents());
}
@Provide
Arbitrary<Money> money() {
return Arbitraries.longs().between(0, 1_000_000_00L).map(Money::ofCents);
}
}
A failure such as additionNeverLosesCents shrinking to a = 0.00, b = 0.01 points
straight at a rounding bug no curated example table contained.
Related
- Testing in Java — strategies & tooling map — parent map of the strategy portfolio.
- Unit testing — the example-based layer properties extend; same JUnit 5 build and IDE story.
- TDD — properties slot into the red/green loop once an invariant is articulated.