Hire Me
← All Writing Testing

Property-Based Testing in Java with jqwik

How to write property-based tests in Java with jqwik — generating hundreds of inputs automatically, expressing invariants rather than examples, and finding the edge cases you didn't think to write.

Example-based testing checks that your code produces the right output for inputs you thought of. Property-based testing generates hundreds or thousands of inputs automatically and checks that a property holds for all of them — an invariant, a relationship, a symmetry. The difference is that the test framework finds edge cases you would never have written: zeros, negatives, empty strings, MAX_INT, special Unicode characters.

jqwik is the leading property-based testing library for Java, integrating with JUnit 5.

Dependency

<dependency>
    <groupId>net.jqwik</groupId>
    <artifactId>jqwik</artifactId>
    <version>1.9.0</version>
    <scope>test</scope>
</dependency>

A first property

Instead of “given input 10.0, output is 10.0 * 1.1”, express the invariant: “the result is always greater than the input for any positive amount.”

import net.jqwik.api.*;

class PricingServiceTest {

    @Property
    void markup_alwaysIncreasesPrice(@ForAll @Positive double amount) {
        double result = pricingService.addMarkup(amount);
        assertThat(result).isGreaterThan(amount);
    }
}

jqwik generates 1000 random positive doubles and verifies the property for each. If any fails, it shrinks the counterexample to the simplest input that still fails — rather than reporting 42.7834926, it will report 0.1 if that’s the minimal failing case.

Combining with JUnit 5

Add @ExtendWith(JqwikExtension.class) or configure via junit-platform.properties:

# src/test/resources/junit-platform.properties
jqwik.tries.default=1000
jqwik.database=.jqwik-database

The .jqwik-database stores previously failing inputs and re-runs them on the next test pass — if a failure was found and you fix it, the fix is verified against the exact counterexample.

Arbitraries: controlling generation

Built-in annotations:

@Property
void orderValidation(
    @ForAll @Positive double price,
    @ForAll @DoubleRange(min = 0.0, max = 10000.0) double size,
    @ForAll @From("validSides") String side
) {
    // test body
}

@Provide
Arbitrary<String> validSides() {
    return Arbitraries.of("BACK", "LAY");
}

For domain objects, use Arbitrary combinators:

@Provide
Arbitrary<Order> orders() {
    Arbitrary<String> marketIds = Arbitraries.strings()
        .withCharRange('0', '9')
        .ofLength(7)
        .map(s -> "1." + s);

    Arbitrary<Double> prices = Arbitraries.doubles()
        .between(1.01, 1000.0);

    Arbitrary<Double> sizes = Arbitraries.doubles()
        .between(2.0, 100.0);

    return Combinators.combine(marketIds, prices, sizes)
        .as((id, price, size) -> new Order(id, price, size, "BACK"));
}

@Property
void orderInvariant(@ForAll("orders") Order order) {
    assertThat(order.liability()).isGreaterThanOrEqualTo(0.0);
}

Round-trip properties

Serialisation/deserialisation is a perfect property test candidate:

@Property
void jsonRoundTrip(@ForAll("orders") Order order) throws Exception {
    String json    = objectMapper.writeValueAsString(order);
    Order restored = objectMapper.readValue(json, Order.class);
    assertThat(restored).isEqualTo(order);
}

This catches any field that is serialised but not deserialised correctly — including null handling, precision loss on doubles, and timezone issues on dates — across a large generated sample.

Invariant properties

Test mathematical invariants:

@Property
void moneyAdditionIsCommutative(
    @ForAll @LongRange(min = 1, max = 100_000) long amountA,
    @ForAll @LongRange(min = 1, max = 100_000) long amountB
) {
    Money a = new Money(amountA, Currency.GBP);
    Money b = new Money(amountB, Currency.GBP);

    assertThat(a.add(b)).isEqualTo(b.add(a));
}

@Property
void moneyAdditionIsAssociative(
    @ForAll @LongRange(min = 1, max = 10_000) long x,
    @ForAll @LongRange(min = 1, max = 10_000) long y,
    @ForAll @LongRange(min = 1, max = 10_000) long z
) {
    Money a = new Money(x, Currency.GBP);
    Money b = new Money(y, Currency.GBP);
    Money c = new Money(z, Currency.GBP);

    assertThat(a.add(b).add(c)).isEqualTo(a.add(b.add(c)));
}

Commutativity and associativity are hard to verify exhaustively with example tests but trivial to express as properties.

Shrinking in practice

When a property fails, jqwik shrinks the counterexample:

Falsified! ...
  org.opentest4j.AssertionFailedError:
    Parameter #0 [price]: 0.010000000000000002
    (original failure: 347.289276843...)

The minimal counterexample is far more useful for debugging than the original random input. This is the most valuable feature of property-based testing — it finds the smallest input that exposes the bug.

When to use property-based testing

Property tests complement example tests — they do not replace them. Use properties for:

Use example tests for: specific business rules, known edge cases, documentation of expected behaviour.

If you’re writing Java services and want to improve your test coverage quality with property-based testing, get in touch.

Samuel Jackson

Samuel Jackson

Senior Java Back End Developer & Contractor

Senior Java Back End Developer — Betfair Exchange API specialist, Spring Boot, AWS, and event-driven architecture. 20+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.