From qa-property-based
Authors property-based tests for the JVM (Java + Kotlin) using jqwik - wires `@Property` test methods, `@ForAll` parameter annotations, `Arbitraries.integers/strings/etc` generators, custom `@Provide` arbitraries, and the JUnit 5 platform integration. Use when a JVM project needs PBT - alternative to JUnit-QuickCheck and Vavr's property-checking; tightly integrates with JUnit 5 so property tests run alongside conventional unit tests in the same Maven / Gradle pipeline.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-property-based:jqwik-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Per [jqwik-home][jh]:
Per jqwik-home:
"jqwik is a JVM library enabling property-based testing (PBT) in Java and Kotlin. ... [It] combines intuitive microtests with randomized test data generation."
Properties describe "a generic invariant or post condition of your code, given some precondition" (jqwik-home). The library "integrates with JUnit 5, allowing property tests to run alongside conventional unit tests" (jqwik-home).
Maven:
<dependency>
<groupId>net.jqwik</groupId>
<artifactId>jqwik</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>
Per jqwik-home, 1.9.3 is the latest as of the source-fetch. The artifact pulls in the JUnit 5 platform dependency transitively.
Gradle:
testImplementation 'net.jqwik:jqwik:1.9.3'
test {
useJUnitPlatform()
}
Per jqwik-home:
import net.jqwik.api.*;
class CartProperties {
@Property
boolean addingItemIncreasesCount(@ForAll int qty) {
Cart cart = new Cart();
cart.addItem(new Item("BOOK-001", qty));
return cart.itemCount() == qty;
}
}
The @Property annotation marks a property test (JUnit 5 hooks in
via the platform). @ForAll on a parameter means "jqwik generates
values for this." The method returns boolean (or void with
assertions).
By default jqwik runs each property with 1000 generated cases (more than Hypothesis's 100 or proptest's 256).
@Property
boolean validQtyStaysInRange(@ForAll @IntRange(min = 1, max = 100) int qty) {
Cart cart = new Cart();
cart.addItem(new Item("BOOK-001", qty));
return cart.itemCount() >= 1 && cart.itemCount() <= 100;
}
Constraint annotations (in net.jqwik.api.constraints.*):
| Annotation | Purpose |
|---|---|
@IntRange(min, max) | Integers in range |
@LongRange(min, max) | Longs in range |
@DoubleRange(min, max) | Doubles in range |
@StringLength(min, max) | Strings of bounded length |
@AlphaChars / @NumericChars | String character class |
@Size(min, max) | Collection size |
@NotEmpty | Non-empty collection / string |
@NotBlank | Non-whitespace string |
@Email | Email-shaped string |
@MinList / @MaxList | List bounds |
@UniqueElements | Unique-element collections |
@Positive / @Negative | Positive / negative number |
@Provideclass UserProperties {
@Property
boolean userJsonRoundTrip(@ForAll("validUser") User u) {
String json = mapper.writeValueAsString(u);
User back = mapper.readValue(json, User.class);
return u.equals(back);
}
@Provide
Arbitrary<User> validUser() {
Arbitrary<Long> id = Arbitraries.longs().between(1, 1_000_000);
Arbitrary<String> email = Arbitraries.strings()
.alpha().ofMinLength(3).ofMaxLength(10)
.map(s -> s + "@example.com");
Arbitrary<Integer> age = Arbitraries.integers().between(18, 100);
return Combinators.combine(id, email, age).as(User::new);
}
}
The @Provide method returns an Arbitrary<T>; its name (or
the method name when omitted) is referenced from @ForAll("...").
Combinators.combine(...) is the canonical way to assemble a
multi-field arbitrary; .as(...) constructs the target object
from the parts.
Per jqwik-home, the Arbitraries API provides "Built-in
generators like Arbitraries.integers() for creating constrained
data sets":
Arbitraries.integers() // any int
.between(0, 100) // bounded
.greaterOrEqual(0); // half-bounded
Arbitraries.strings() // any string
.alpha() // alphabetic only
.ofLength(8); // fixed length
Arbitraries.of(Status.ACTIVE, Status.SUSPENDED, Status.NOT_VERIFIED); // enum-like
Arbitraries.subsetOf(allRoles); // subset of a known set
For collections:
Arbitraries.integers().list() // List<Integer>
.ofMinSize(1).ofMaxSize(100);
Arbitraries.strings().set(); // Set<String>
Arbitraries.maps(
Arbitraries.strings().alpha(),
Arbitraries.integers().between(0, 100)
);
@Property
@Statistics
boolean evenAndOddDistribution(@ForAll int n) {
Statistics.label("evenness").collect(n % 2 == 0 ? "even" : "odd");
return true;
}
Statistics output reports the distribution of generated cases - useful for verifying the strategy generates the intended mix.
@Property(tries = 5000, shrinking = ShrinkingMode.FULL, edgeCases = EdgeCasesMode.MIXIN)
boolean expensiveProperty(@ForAll long n) {
// ...
}
| Field | Default | Use |
|---|---|---|
tries | 1000 | More for higher confidence. |
shrinking | FULL | FULL (default) / BOUNDED / OFF. |
edgeCases | MIXIN | Mix in edge cases (0, MAX, MIN, ε, etc.) by default. |
seed | random | Fixed seed for CI determinism. |
maxDiscardRatio | 5 | Cap on assume() rejection ratio. |
For CI determinism:
@Property(seed = "42")
boolean reproducibleProperty(@ForAll int n) { /* ... */ }
Or globally via jqwik.config.properties:
defaultSeed = 42
defaultTries = 1000
mvn test # runs JUnit 5 tests including @Property
gradle test # same
No additional config beyond JUnit 5 platform.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Returning void without assertions | Test always passes; property is silent. | Return boolean OR throw on failure (e.g. JUnit assertTrue). |
@Provide arbitrary that filters most cases | Discard ratio exceeded; jqwik fails the property. | Constrain at the Arbitrary level (Step 3 annotations + .between(...)). |
| Sharing mutable state across @Property iterations | Test depends on iteration order; flaky. | Construct fresh state per call (within the property method body). |
| Hard-coded seed in production property | Loses randomization; misses regressions. | Random seed locally; fixed seed in CI only (Step 7). |
tries = 100_000 on a slow property | CI never finishes. | Budget per total runtime / tries. |
| Asserting on specific values inside the property | Defeats PBT. | Properties assert relationships. |
Skipping @Provide for one-off domain objects | Inline construction is verbose; harder to reuse. | Move to @Provide even for one-property arbitraries. |
BOUNDED shrinking caps
iterations; OFF skips entirely.jqwik-kotlin extension.runBlocking (Kotlin) or explicit Future.get().@Property, @ForAll, @Provide,
Arbitraries API, JUnit 5 integration, version 1.9.3.hypothesis-testing,
fast-check-testing,
proptest-testing,
quickcheck-testing -
per-language siblings.npx claudepluginhub testland/qa --plugin qa-property-basedProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.