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>
<groupId>net.jqwik</groupId>
<artifactId>jqwik</artifactId>
<version>1.9.0</version>
<scope>test</scope>
</dependency>
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.
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.
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);
}
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.
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.
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.
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.