NWave nw-pbt-jvm
JVM property-based testing with jqwik, ScalaCheck, and ZIO Test frameworks
install
source · Clone the upstream repo
git clone https://github.com/nWave-ai/nWave
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/nWave-ai/nWave "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/nw/skills/nw-pbt-jvm" ~/.claude/skills/nwave-ai-nwave-nw-pbt-jvm-6dd65d && rm -rf "$T"
manifest:
plugins/nw/skills/nw-pbt-jvm/SKILL.mdsource content
PBT JVM -- jqwik (Java/Kotlin) + ScalaCheck + ZIO Test
Framework Selection
| Framework | Language | Shrinking | Stateful | Choose When |
|---|---|---|---|---|
| jqwik | Java/Kotlin | Integrated | Yes (actions) | Java/Kotlin projects (recommended default) |
| ScalaCheck | Scala | Type-based | Yes (Commands) | Scala projects (established choice) |
| ZIO Test | Scala | Integrated | Via effects | ZIO-based Scala projects |
Quick Start (jqwik)
import net.jqwik.api.*; class SortProperties { @Property void sortPreservesLength(@ForAll List<Integer> list) { List<Integer> sorted = new ArrayList<>(list); Collections.sort(sorted); Assertions.assertEquals(list.size(), sorted.size()); } } // Run: ./gradlew test (or mvn test)
Generator (Arbitrary) Cheat Sheet (jqwik)
@ForAll int x // any int @ForAll @IntRange(min = 0, max = 99) int x @ForAll @StringLength(min = 1, max = 50) String s @ForAll @Size(min = 1, max = 10) List<Integer> list // Custom provider @Provide Arbitrary<String> emails() { return Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(10) .map(name -> name + "@example.com"); } // Combinators Arbitraries.integers().between(0, 100) Arbitraries.of("a", "b", "c") Arbitraries.frequencyOf(Tuple.of(80, Arbitraries.integers()), Tuple.of(20, Arbitraries.just(0))) // Combine Combinators.combine( Arbitraries.strings().alpha().ofMinLength(1), Arbitraries.integers().between(1, 120) ).as((name, age) -> new User(name, age)) // Recursive Arbitraries.recursive( () -> Arbitraries.of(JsonValue.NULL, JsonValue.TRUE), inner -> Arbitraries.maps(Arbitraries.strings(), inner).map(JsonValue::fromMap), 5 )
Stateful Testing (jqwik)
@Property void storeMatchesModel(@ForAll("storeActions") ActionSequence<MyStore> actions) { actions.run(new MyStore()); } @Provide ActionSequenceArbitrary<MyStore> storeActions() { return Arbitraries.sequences( Arbitraries.oneOf( Combinators.combine(Arbitraries.strings(), Arbitraries.integers()) .as(PutAction::new), Arbitraries.strings().map(GetAction::new) ) ); } class PutAction implements Action<MyStore> { final String key; final int value; PutAction(String key, int value) { this.key = key; this.value = value; } @Override public MyStore run(MyStore store) { store.put(key, value); return store; } }
Quick Start (ScalaCheck)
import org.scalacheck.Prop.forAll val propSortLength = forAll { (xs: List[Int]) => xs.sorted.length == xs.length } // With ScalaTest class SortSpec extends AnyFunSuite with ScalaCheckPropertyChecks { test("sort idempotent") { forAll { (xs: List[Int]) => xs.sorted.sorted shouldBe xs.sorted } } }
ScalaCheck Generators
Gen.choose(0, 100) // bounded int Gen.alphaStr // alphabetic string Gen.listOf(Gen.posNum[Int]) // list Gen.oneOf(Gen.const(1), Gen.const(2)) // union Gen.frequency((80, Gen.alphaChar), (20, Gen.numChar)) Gen.recursive[Tree](gen => Gen.oneOf(Gen.const(Leaf), for { l <- gen; r <- gen; v <- Gen.posNum[Int] } yield Node(v, l, r)) )
ScalaCheck Stateful (Commands)
object StoreSpec extends Commands { type State = Map[String, Int]; type Sut = MyStore def genCommand(state: State): Gen[Command] = Gen.oneOf( for { k <- Gen.alphaStr; v <- Gen.posNum[Int] } yield Put(k, v), Gen.oneOf(state.keys.toSeq).map(Get(_)) ) }
Quick Start (ZIO Test)
import zio.test._ test("sort preserves length") { check(Gen.listOf(Gen.int)) { xs => assertTrue(xs.sorted.length == xs.length) } }
ZIO Test Generator Cheat Sheet
Gen.int // any Int Gen.int(0, 100) // bounded Gen.double // any Double Gen.string // any String Gen.alphaNumericString Gen.boolean Gen.listOf(Gen.int) // List[Int] Gen.setOf(Gen.string) // Set[String] Gen.mapOf(Gen.string, Gen.int) // Map[String, Int] Gen.option(Gen.int) // Option[Int] Gen.oneOf(Gen.const(1), Gen.const(2)) // union Gen.weighted((Gen.int, 80.0), (Gen.const(0), 20.0)) // weighted // Custom val genUser = for { name <- Gen.alphaNumericString age <- Gen.int(1, 120) } yield User(name, age)
ZIO Test stateful testing: Use
ZIO.stateful with Ref-based model state in effect composition.
Test Runner Integration
<!-- jqwik (Maven) --> <dependency> <groupId>net.jqwik</groupId><artifactId>jqwik</artifactId> <version>1.8.0</version><scope>test</scope> </dependency>
// ScalaCheck (build.sbt) libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.17.0" % Test // ZIO Test: "dev.zio" %% "zio-test" % "2.x" % Test
Unique Features
jqwik
- Edge cases: Automatically tests boundary values (0, MIN/MAX, empty)
- @StatisticsReport: Shows distribution of generated values
- Domains: Group related arbitraries into reusable contexts
- Kotlin support: Works natively via JUnit 5
ScalaCheck
- Type-class based:
for automatic derivationArbitrary[T] - Parallel Commands: Stateful testing with parallel execution
- Shrink[T]: Separate shrink type class (can shrink past generator constraints)