rgoussu@goussu: ~/library/java/testing
~/library/java/testing cat property-based-testing.md

Property-based testing — jqwik

# Testing invariants over generated inputs with jqwik — properties, generators, shrinking, and model-based stateful testing on JUnit 5.

Conceptsaved 2026-08-09 #java#testing#jqwik#property-based#quality

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 returns boolean/uses assertions; jqwik runs it 1000 times by default with varied inputs.
  • Generators: built-in Arbitraries for primitives, strings, collections; constrain via annotations (@IntRange, @AlphaChars, @Size) or compose programmatically with @Provide methods (map, flatMap, filter, Combinators.combine).
  • Shrinking is the killer feature: on failure, jqwik searches for a minimal failing input — you debug "" or 0, 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