javaspec is a spec-first BDD tool for Java, inspired by PHPSpec. You write subject-centric it_* examples with let, beConstructedWith, and generated typed subject proxies such as total(...).shouldReturn(expected), run the specification, and let javaspec guide the next small production-code step.
The core is Java 8-compatible and has no third-party runtime dependencies. It can be used directly from the CLI, embedded through a no-System.exit launcher, or adopted through optional Maven, Gradle, and JUnit Platform adapters.
Artifacts use the Maven Central group io.github.jvmspec. The active 1.0.0-RC5 release candidate
is available from Maven Central:
<dependency>
<groupId>io.github.jvmspec</groupId>
<artifactId>javaspec</artifactId>
<version>1.0.0-RC5</version>
<scope>test</scope>
</dependency>For snapshots, use the Central Portal Snapshots repository.
The Gradle plugin id is io.github.jvmspec. RC1 was submitted successfully with description
"Optional Gradle adapter for the javaspec runner" and is awaiting first-publication approval; use
the included build until 1.0.0-RC5 appears on the Gradle Plugin Portal.
- Spec-first disciplined TDD/BDD workflow inspired by PHPSpec.
- Java 8-compatible core.
- Zero-runtime-dependency core artifact.
- CLI, Maven plugin, Gradle plugin, and JUnit Platform adapter.
- Generation and update support for specs, support classes, production skeletons, constructors, and methods.
- JSON and JUnit XML-compatible reports.
- Recommended PHPSpec-like authoring with
it_*examples,let,beConstructedWith, and generated*SpecSupportproxy methods such asmethod().shouldReturn(expected). - Interface doubles in core; optional ByteBuddy-based concrete-class doubles adapter.
Build the local release candidate and make the launcher available:
# Build and install the local release candidate
mvn -q -DskipTests install
# Add bin/ to your PATH for this session
export PATH="$PWD/bin:$PATH"
# Or invoke directly
./bin/javaspec --helpAfter adding bin/ to your PATH, the javaspec command is available. You can also run ./bin/javaspec directly from the repository root without modifying PATH.
Step 1 — Describe a class: creates spec and support skeletons.
javaspec describe com.example.PriceCalculatorStep 2 — View and edit the generated spec: open src/test/java/spec/com/example/PriceCalculatorSpec.java and add one example.
package spec.com.example;
import com.example.PriceCalculator;
public class PriceCalculatorSpec extends PriceCalculatorSpecSupport {
public void it_calculates_the_total_price() {
total(10.0, 2.5).shouldReturn(12.5);
}
}The standard concrete-spec style is PHPSpec-like: write one behavior method, call the generated typed proxy (total(...).shouldReturn(...)), and let the generated *SpecSupport class stay in the background. Regenerate support whenever the subject signature changes.
Step 3 — Run specs: the generation prompt fires because the production class is missing. --generate accepts automatically; --compile recompiles before execution; --formatter pretty shows descriptive output.
javaspec run --generate --compile --formatter prettySample output:
PriceCalculator
✗ it calculates the total price [BROKEN: class not found]
describes missing class: com.example.PriceCalculator
Generated class skeleton: src/main/java/com/example/PriceCalculator.java
Compilation output: target/javaspec-classes
✓ it calculates the total price [PASSED]
1 spec, 1 example — 1 passed, 0 failed, 0 broken, 0 skipped, 0 pending
Step 4 — View the generated production class:
cat src/main/java/com/example/PriceCalculator.javajavaspec wrote a skeleton with a stub return value (return 0.0d;) and a // javaspec:stub marker. Generated production stubs are mechanical scaffolding, not domain logic: while any marker remains, a compiled run reports a synthetic broken generation result and exits non-zero so a default return value cannot become an accidental GREEN.
Step 5 — Implement the body (minimal fix):
sed -i 's/return 0.0d;/return arg0 + arg1;/' src/main/java/com/example/PriceCalculator.javaThe fixed method now looks like:
public double total(double arg0, double arg1) {
return arg0 + arg1;
}Step 6 — Verify everything passes:
javaspec run --compile --formatter prettyIf you have a Maven project with the javaspec Maven plugin configured, mvn javaspec:run works too.
That's the full red-green cycle with javaspec.
Use javaspec when you want to:
- start with executable behavior examples before production implementation;
- keep test/runtime infrastructure small and dependency-light;
- generate boring spec/support/production skeletons while you focus on behavior;
- run the same specs from the CLI, Maven, Gradle, or JUnit Platform-based tools;
- produce machine-readable JSON and CI-friendly JUnit XML-compatible reports;
- use built-in expectations and interface doubles without requiring JUnit or a mocking framework.
A javaspec spec is a Java class whose public no-argument methods are examples. javaspec describe creates a concrete *Spec plus a generated *SpecSupport; the support class extends ObjectBehavior<T>, while the concrete spec stays focused on behavior.
public class CalculatorSpec extends CalculatorSpecSupport {
public void it_adds_two_numbers() {
add(2, 3).shouldReturn(5);
}
}That concise form is the standard PHPSpec-like style. The generated support class provides typed proxy methods for each subject method — each returns Matchable<R> and can be chained directly. Generate or refresh *SpecSupport before compiling a concrete spec whose subject API changed.
Common authoring concepts:
- Generated typed proxies (
add(2, 3).shouldReturn(5)) are the standard subject-call syntax. subject()lazily creates the described object behind generated support and advanced fallback APIs.beConstructedWith(...)selects constructor arguments before the generated proxy first accesses the subject.beConstructedThrough("factoryName", ...)selects a static factory method.@Skip,@Pending,skip(...), andpending(...)mark examples intentionally not executed.
After building from the repository root:
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main --help
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main describe com.example.Calculator
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main run --compile --generateUseful run options:
--config <file> # load javaspec configuration
--suite <name> # select a configured suite
--spec-dir <dir> # spec source root
--source-dir <dir> # production source root
--classpath <path-list> # explicit runtime/dependency classpath
--classpath-file <file> # one classpath entry per line
--resolve-pom <pom.xml> # resolve runtime deps from POM (offline, local repo)
--compile # compile source/spec trees before execution
--compile-output <dir> # compile output directory; implies --compile
--release <N> # Java release target for --compile (e.g. 8, 11, 17)
--generate # apply generation/update prompts non-interactively
--dry-run # plan generation/update work without writes
--stop-on-failure # stop after first failed or broken example
--formatter progress|pretty|json # select built-in output format; json owns stdout exclusively
--profile java8|java11|java17|java21|java25
--report <file> # executable-example JSON report
--generation-report <file> # deterministic generation outcome report, including failures
--junit-xml <file> # JUnit XML-compatible report
--class <name> # filter described/spec class
--example <name> # filter example method/display name/order indexOther commands:
javaspec list-extensions # list discovered formatters and extensions + classpath hints
javaspec prophesize <Class> # generate typed Prophecy wrapper for an interface or concrete classExit codes are stable: 0 for success, 1 for failed/broken examples or declined/pending generation work, 64 for usage/profile/compiler/bootstrap errors, and 70 for I/O failures.
Initial section 1 manual pages are maintained under docs/man/ in English,
Italian, Spanish, German, French, and Simplified Chinese. Preview one without installing it:
man -l docs/man/en/man1/javaspec.1
man -l docs/man/it/man1/javaspec.1Validate every translation and its shared command-contract tokens with:
scripts/check-man-pages.shCore and the Maven plugin are available directly from Maven Central. No local javaspec installation
is required for a consuming project. Consumer pom.xml example:
<properties>
<javaspec.version>1.0.0-RC5</javaspec.version>
</properties>
<dependencies>
<dependency>
<groupId>io.github.jvmspec</groupId>
<artifactId>javaspec</artifactId>
<version>${javaspec.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.github.jvmspec</groupId>
<artifactId>javaspec-maven-plugin</artifactId>
<version>${javaspec.version}</version>
<executions>
<execution>
<id>generate-javaspec-support</id>
<phase>generate-test-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<profile>java21</profile>
</configuration>
</execution>
<execution>
<id>run-javaspec</id>
<phase>verify</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<jsonReportFile>${project.build.directory}/javaspec/run-report.json</jsonReportFile>
<junitXmlReportFile>${project.build.directory}/javaspec/junit-report.xml</junitXmlReportFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>The generate goal discovers specification source without first compiling it, writes base typed
*SpecSupport classes under target/generated-sources/javaspec, and registers that directory as a
Maven test source root before testCompile. It does not require tracked generated sources or a
separate build-helper-maven-plugin binding. Keep the core dependency and both plugin goals on the
same immutable ${javaspec.version}; select the project's Java profile explicitly when it is newer
than Java 8.
See examples/maven-basic/ for a complete consumer project.
The Gradle plugin id is io.github.jvmspec. The corrected RC1 submission completed successfully in
workflow run 29148854181, but the
first publication is still awaiting Plugin Portal approval. Until the public marker resolves, use
the included plugin build shown by
examples/gradle-basic/settings.gradle:
pluginManagement {
includeBuild('../../javaspec-gradle-plugin')
repositories {
gradlePluginPortal()
mavenLocal()
mavenCentral()
}
}Consumer build.gradle with the included build:
plugins {
id 'java'
id 'io.github.jvmspec'
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
testImplementation 'io.github.jvmspec:javaspec:1.0.0-RC5'
}
javaspec {
jsonReportFile = file("$buildDir/reports/javaspec/run-report.json")
junitXmlReportFile = file("$buildDir/reports/javaspec/junit-report.xml")
}
tasks.named('javaspecRun') {
stopOnFailure = true
failOnFailure = true
}Run it with:
gradle -p examples/gradle-basic clean javaspecRunAfter Portal approval, an external consumer can remove includeBuild(...) and use:
plugins {
id 'java'
id 'io.github.jvmspec' version '1.0.0-RC5'
}The optional JUnit Platform engine is available from Maven Central. Consumer Maven example:
<dependency>
<groupId>io.github.jvmspec</groupId>
<artifactId>javaspec</artifactId>
<version>1.0.0-RC5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.jvmspec</groupId>
<artifactId>javaspec-junit-platform-engine</artifactId>
<version>1.0.0-RC5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.10.2</version>
<scope>test</scope>
</dependency>Configure your JUnit Platform launcher, IDE, or Surefire setup to include *Spec.java. The stable 1.0 engine id, selector, unique-id, source, row, status-mapping, and IDE boundaries are documented in docs/junit-platform-contract-1.0.md. See examples/junit-platform-basic/ and javaspec-junit-platform-engine/README.md.
Build tools and custom launchers can call javaspec without System.exit:
SpecDiscoveryRequest request = SpecDiscoveryRequest.of(new File("src/test/java"));
JavaspecInvocation invocation = JavaspecInvocation.discovering(request, classLoader)
.withStopOnFailure(true);
JavaspecInvocationResult result = JavaspecLauncher.run(invocation);
int exitCode = result.exitCode();Generated typed proxy methods are the standard JavaSpec syntax for subject behavior:
add(2, 3).shouldReturn(5);
name().shouldStartWith("calc");
items().shouldHaveCount(3);The lower-level match(...) wrapper remains available before generated support exists, for arbitrary
non-subject values, and for programmatic custom matchers. Do not use it as the ordinary subject-call
style once the generated proxy is available:
match(subject().add(2, 3)).shouldReturn(5);ObjectBehavior also has direct convenience assertions such as shouldReturn(actual, expected) for
ad-hoc non-subject checks. They are not the standard style for ordinary subject behavior examples.
Available expectation families include:
- equality/identity:
shouldReturn,shouldEqual,shouldBe, and negated aliases; - type checks:
shouldHaveType,shouldBeAnInstanceOf,shouldImplement; - numeric approximation:
shouldBeApproximately(expected, tolerance),shouldReturnApproximately(...), and negated aliases; - generated object-state helpers:
shouldBeActive()/shouldNotBeActive()for boolean accessors andshouldHaveTitle(expected)/shouldNotHaveTitle(unexpected)for value accessors; - strings, collections, maps, arrays, iterables, and iterators: contain/count/empty/key/value checks;
- string helpers: starts-with, ends-with, and regular-expression checks;
- exception expectations through
shouldThrow(...).during...support methods; - structural markers used by generation, such as
shouldBeAnInterface()andshouldImplement(...).
Custom matcher 1.0 scope is programmatic: register Matcher/CustomMatcher instances in the
MatcherRegistry and call match(actual).shouldMatch("name", args...). Configuration-file matcher
registration and generated typed custom matcher methods are deferred; see
docs/matcher-contract-1.0.md.
Configure construction before calling subject():
public void it_uses_constructor_arguments() {
beConstructedWith("USD", 2);
currency().shouldReturn("USD");
}
public void it_uses_a_factory() {
beConstructedThrough("from", "42");
value().shouldReturn(42);
}Generation can preserve, comment, or delete constructor-related skeleton code according to the selected constructor policy. Constructor identity follows Java overload rules: declaring type plus ordered canonical erased parameter types, with varargs normalized to arrays; parameter names and bodies are not identity. Package-private and generic constructors are preserved, and types with the same simple name in different packages remain distinct. Updates to existing production source are planned in memory and require --generate or affirmative interactive authorization before the single atomic write.
JavaSpec example data is the Java 8 adaptation of PHPSpec example tables. Use it when one behavior
needs a few concrete cases but a Cucumber Scenario Outline or JUnit parameterized test would add
ceremony. The public it_* method remains the behavior example; rows execute inside that example and
failing rows include row context in the assertion message.
public void it_normalizes_known_inputs() {
examples(row(" Alice ", "Alice"), row("Bob", "Bob"))
.verify((input, expected) -> normalize(input).shouldReturn(expected));
}Example1 and Example2 are JavaSpec core functional interfaces, so the standard authoring form is
a Java 8 lambda with no Jupiter dependency. Unlike an anonymous inner class, the lambda preserves the
enclosing spec scope for source discovery; normalize(...) is therefore discovered and generated as
the typed support proxy. JSON/JUnit XML/JUnit Platform row reporting and
selector boundaries are frozen
in docs/example-data-contract-1.0.md; row selectors filter adapter descriptors/events and do not
turn rows into isolated Jupiter parameterized invocations.
Core doubles use JDK dynamic proxies and require no extra dependencies:
InterfaceDouble<Notifier> notifier = interfaceDouble(Notifier.class);
notifier.control().returns("send", true);
beConstructedWith(notifier.instance());
notify("hello").shouldReturn(true);
notifier.control().verifyCalled("send", "hello");Argument matchers, throwing stubs, and answer callbacks are supported:
import static io.github.jvmspec.doubles.Doubles.any;
import static io.github.jvmspec.doubles.Doubles.eq;
notifier.control().returnsFor("send", true, eq("alerts"), any(String.class));
notifier.control().verifyCalled("send", eq("alerts"), any(String.class));Core doubles support ordinary interfaces only. Concrete classes, final classes, static methods, and constructors are not mocked by the core runtime.
Advanced stubbing APIs (all zero-dependency, all in core):
// Sequential returns — each call returns the next value; last value repeats
notifier.control().when("send").thenReturn(true, false, true);
// Exhaustion policy — return values then throw
notifier.control().when("fetch").thenReturnThenThrow(new NoSuchElementException(), "a", "b");
// Sequential answer callbacks
notifier.control().when("transform").thenAnswerSequence(
inv -> "first:" + inv.argument(0),
inv -> "second:" + inv.argument(0)
);
// Argument captor
ArgumentCaptor<String> captor = ArgumentCaptor.create();
notifier.control().when("send", captor).thenReturn(true);
notifier.instance().send("hello");
shouldReturn(captor.value(), "hello"); // direct helper for a non-subject value
// Ordered verification
notifier.control().verifyInOrder("prepare", "send", "cleanup");
notifier.control().verifyCalledBefore("prepare", "send");Install and add the standalone adapter only when you need non-final concrete-class doubles:
mvn -q -f javaspec-bytecode-doubles/pom.xml -DskipTests install<dependency>
<groupId>io.github.jvmspec</groupId>
<artifactId>javaspec-bytecode-doubles</artifactId>
<version>1.0.0-RC5</version>
<scope>test</scope>
</dependency>Example:
import io.github.jvmspec.doubles.Doubles;
import io.github.jvmspec.doubles.InterfaceDouble;
InterfaceDouble<DataStore> storeDouble = Doubles.concreteDouble(DataStore.class);
storeDouble.control().returns("save", true);
beConstructedWith(storeDouble.instance());
save("item").shouldReturn(true);The adapter is ByteBuddy-based and lives outside the core artifact. It supports non-final concrete classes only and explicitly rejects final classes, enums, arrays, annotations, primitives, and interfaces. See examples/bytecode-doubles-basic/.
Install and add the standalone agent adapter when you need final-class, static-method, or construction-aware doubles:
mvn -q -f javaspec-bytecode-agent/pom.xml -DskipTests install<dependency>
<groupId>io.github.jvmspec</groupId>
<artifactId>javaspec-bytecode-agent</artifactId>
<version>1.0.0-RC5</version>
<scope>test</scope>
</dependency>The module supports dynamic self-attach through ByteBuddy Agent when the JVM allows it. You can also
start tests with -javaagent:javaspec-bytecode-agent.jar to make instrumentation available before
execution. In ordinary behavior examples, assert through generated subject proxies. The low-level
adapter checks below inspect non-subject values directly, so they use JavaSpec's explicit convenience
helper shouldReturn(actual, expected) instead:
// Final concrete class instance-method double
InterfaceDouble<FinalGreeter> greeter = Doubles.concreteDouble(FinalGreeter.class);
greeter.when("greet", "Ada").thenReturn("stubbed Ada");
shouldReturn(greeter.instance().greet("Ada"), "stubbed Ada");
// Static method double; close() restores original behavior for later calls
try (StaticDouble<StaticUtility> statics = BytecodeAgentDoubles.staticDouble(StaticUtility.class)) {
statics.when("message", "x").thenReturn("stubbed x");
shouldReturn(StaticUtility.message("x"), "stubbed x");
}
// Construction-aware double; subsequently created instances are registered
try (ConstructionDouble<ConstructedGreeter> construction =
BytecodeAgentDoubles.mockConstruction(ConstructedGreeter.class)) {
construction.when("name").thenReturn("stubbed");
shouldReturn(new ConstructedGreeter().name(), "stubbed");
}While a static double is active, unstubbed static calls return normal javaspec default values
(null, 0, false, etc.) instead of executing the original method. Closing the handle removes the
static/construction registration; the class remains instrumented, but unregistered calls fall through
to original behavior. See examples/bytecode-agent-basic/ for a
Maven example covering final-class and static-method doubles.
Inspired by phpspec/prophecy, javaspec provides a declarative doubles API built around prophecies, promises, and predictions.
Doubles replace dependencies of the subject under test — interfaces that the subject collaborates with — not the subject itself.
The recommended PHPSpec-like collaborator style is the generated typed *Prophecy wrapper:
write MailerProphecy mailer = prophesizeMailer();, configure promises with
mailer.send(...).willReturn(...), inject mailer.reveal(), then add predictions such as
mailer.send(...).shouldBeCalled(). The lower-level reflective mailer.method("send", ...) form is
available as a bootstrap/fallback API, but should not be the primary style in user specs once the
wrapper has been generated.
For concise, method-name-safe syntax, generate typed *Prophecy wrapper classes using the
reflection-based generator:
javaspec prophesize com.example.MailerThis produces MailerProphecy extends ObjectProphecy<Mailer> with typed delegation methods,
so you can call mailer.send(...) instead of mailer.method("send", ...):
import static io.github.jvmspec.doubles.prophecy.Argument.*;
public class UserServiceSpec extends UserServiceSpecSupport {
public void it_sends_a_welcome_email() {
MailerProphecy mailer = prophesizeMailer();
mailer.send(any(String.class), any(String.class), any(String.class))
.willReturn(true)
.shouldBeCalled();
setMailer(mailer.reveal());
sendWelcomeEmail("user@example.com");
checkPredictions();
}
}On Java 10+, the same generated helper also supports local-variable inference while keeping the same typed PHPSpec-like method syntax:
var mailer = prophesizeMailer();
mailer.send(any(String.class), any(String.class), any(String.class))
.willReturn(true)
.shouldBeCalled();The typed wrapper and the support helper are generated under target/generated-sources/javaspec.
The wrapper is not written to src/; Java 8 specs name the wrapper type explicitly, while Java 10+
specs can hide it with var.
Predictions are checked by calling checkPredictions() at the end of an example, or automatically
when --auto-check-predictions is enabled.
You can also receive supported collaborators as PHPSpec-style parameters on let, examples, and
letGo. For one example run, parameters are resolved in declared method-parameter order and the same
typed prophecy is reused across lifecycle and example methods. Declare each collaborator type at
most once per method; duplicate same-type parameters are reported as ambiguous:
public void let(MailerProphecy mailer) {
mailer.send("user@example.com").willReturn(true).shouldBeCalled();
setMailer(mailer.reveal());
}
public void it_sends_a_welcome_email(MailerProphecy mailer) {
sendWelcomeEmail("user@example.com");
}The core prophecy types live in io.github.jvmspec.doubles.prophecy:
| Type | Purpose |
|---|---|
ObjectProphecy<T> |
Prophecy about an object of type T — wraps an InterfaceDouble<T> produced by core interface doubles or an optional concrete-double adapter |
MethodProphecy<R> |
Prophecy about a specific method call — stub setup and predictions |
Promise<R> |
A promised return value or side-effect (willReturn, willThrow, will) |
Prediction |
A verification that a method was called, not called, or called N times |
PredictionRegistry |
Collects predictions and checks them all at once |
Argument / Arg |
Static matcher DSL (any(), eq(), same(), in(), notIn(), matching(), containingString(), isNull(), notNull()) |
Use ObjectBehavior.prophesize(Class<T>) when the typed wrapper/helper has not been generated yet:
ObjectProphecy<Mailer> mailer = prophesize(Mailer.class);
mailer.method("send", any(String.class), any(String.class), any(String.class))
.willReturn(true)
.shouldBeCalled();A common workflow is to start with the reflective call, run javaspec run --generate, then switch
the concrete spec to the generated MailerProphecy / prophesizeMailer() syntax.
javaspec prophesize <fqcn> # generate wrapper to generated-sources/
javaspec prophesize <fqcn> --output <dir> # custom output directory
javaspec prophesize <fqcn> --package <pkg> # custom target package
javaspec prophesize <fqcn> --overwrite # replace existing file
javaspec prophesize <fqcn> --dry-run # preview without writingWhen javaspec run --generate is used, javaspec scans spec files for prophesize(...) and
prophecy(...) calls, detects missing wrapper classes, and generates them automatically. It also
updates the generated *SpecSupport class with typed helpers such as prophesizeMailer() and
prophecyMailer() under target/generated-sources/javaspec only:
javaspec run --generate --compile --formatter prettyA common workflow is to write prophesize(Mailer.class) first, run generation once, then switch the
spec to the recommended typed helper (MailerProphecy mailer = prophesizeMailer();). Generated typed
wrappers include argument-token overloads, so calls such as mailer.send(any(String.class)) keep the
PHPSpec-like wrapper syntax instead of requiring reflective method("send", ...) fallback.
Import from Argument:
import static io.github.jvmspec.doubles.prophecy.Argument.*;| Matcher | Description |
|---|---|
any() |
Matches any value, including null |
any(Class<?>) |
Matches null or any value assignable to the type |
eq(Object) |
Matches using javaspec's array-aware equality |
isNull() |
Matches only null |
notNull() |
Matches any non-null argument |
containingString(String) |
Matches strings containing a substring |
same(Object) / identicalTo(Object) |
Matches the same object reference |
in(Object...) / notIn(Object...) |
Matches membership using array-aware equality |
matching(Predicate<Object>, String) |
Matches with a custom callback and diagnostic description |
token(ArgumentToken) / custom(ArgumentToken) |
Uses a named custom Prophecy-style token |
Custom prediction callbacks are available when built-in predictions are not expressive enough:
mailer.send(any(String.class)).should(context -> {
if (context.callCount() < 2) {
throw new AssertionError("expected at least two mail sends");
}
});The callback receives matching calls, all calls, the method name, and the argument pattern through
PredictionContext.
Enable automatic prediction verification after each example via CLI flag:
javaspec run --auto-check-predictionsOr programmatically in a spec:
public UserServiceSpec() {
super(UserService.class);
setAutoCheckPredictions(true);
}When enabled, the runner calls checkPredictions() after each example. If any prediction fails,
the example is marked FAILED with a descriptive message.
See examples/prophecy-basic/ for a complete working example with an
interface to prophesize, the recommended typed-wrapper syntax, the reflective bootstrap/fallback
form, and a standalone verification test.
The CLI and adapters can write executable-example JSON, deterministic generation JSON, and JUnit XML-compatible reports. With --formatter json, stdout contains exactly one JSON document; discovery, generation, compilation, and execution diagnostics are written to stderr.
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main run \
--compile \
--report target/javaspec-report.json \
--generation-report target/javaspec-generation-report.json \
--junit-xml target/javaspec-report.xmlNotes:
- Executable-example JSON reports use
schemaVersion: 1and include stable ids, status counts, pending counts, source metadata where available, and optional run metadata/properties. - Generation reports are written for success, dry-run, generation stop, and later pipeline failures. They contain no timestamp, sort paths deterministically, distinguish
PROPOSEDfromAPPLIEDactions, count actual changed-file writes inappliedWrites, and exposeoutcome,exitCode,proceed,pendingGenerationWork, andpendingStubswithout requiring prose parsing. - JUnit XML-compatible reports map skipped and pending examples to
<skipped>elements. - Report write failures exit with code
70. - Schema and golden examples:
docs/schemas/run-report-v1.schema.json,docs/examples/reports/.
Without a config file, javaspec uses conventional defaults:
- suite:
default - spec root:
src/test/java - source root:
src/main/java - spec package prefix:
spec - production package prefix: empty
- profile:
java8 - formatter:
progress - constructor policy:
comment
A small line-based config avoids adding parser dependencies:
profile=java17
formatter=pretty
report=target/javaspec/run-report.json
junit-xml=target/javaspec/junit-report.xml
constructor-policy=comment
suite.domain.spec-dir=src/spec/java
suite.domain.source-dir=src/main/java
suite.domain.spec-package-prefix=spec
suite.domain.package-prefix=com.example
suite.domain.bootstrap=com.example.SpecBootstrapUse it with:
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main run --config javaspec.conf --suite domainCLI options override matching config values where an override exists.
javaspec is classpath/reflection based by default. Source/spec compilation is explicit:
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main run --compile
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main run --compile-output target/javaspec-classes
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main run --compile --release 8
java -cp target/javaspec-1.0.0-RC5.jar io.github.jvmspec.cli.Main run --compile --resolve-pom pom.xmlCompilation uses the current JDK javax.tools.JavaCompiler API. It can use --release <N> on
Java 9+ compilers, caches unchanged source compilation inputs, and can resolve simple Maven POM test
classpath entries from the local Maven repository via --resolve-pom. It still does not fork
javac; Maven and Gradle builds normally rely on their own Java compilation tasks unless their
javaspec adapter settings opt into javaspec compilation.
- The core artifact remains Java 8-compatible and zero-runtime-dependency.
- Maven artifacts use group
io.github.jvmspec;1.0.0-RC5is available from Maven Central. The Gradle Plugin Portal id isio.github.jvmspec; corrected RC1 submission succeeded in workflow run 29148854181 and is awaiting first-publication approval. - The Maven plugin, Gradle plugin, JUnit Platform engine, bytecode doubles adapter, and bytecode agent adapter are standalone optional artifacts outside the root Maven reactor.
- Repository-root
mvn verifyis intentionally core-only. scripts/verify-all.shverifies the core, optional adapters, and standalone examples together.- Non-final concrete-class doubles require the optional ByteBuddy subclass adapter; final-class, static-method, and construction-aware doubles require the optional bytecode agent adapter.
- Compilation is opt-in; it supports local-POM dependency resolution, incremental cache hits, and
--release <N>, but does not forkjavac. - Target profiles (
java8,java11,java17,java21,java25) are enforced conservatively before generation/update writes where metadata is resolvable. - Extension, formatter, bootstrap, parser, and dependency-resolver SPI semantics are frozen in
docs/extension-spi-1.0.md; package scanning, plugin lookup, script engines, typed event model v2, and automatic classpath repair are out of scope.
For day-to-day core verification:
mvn verify
mvn dependency:tree -Dscope=runtimeFor aggregate local verification of core, standalone adapters, and examples:
scripts/verify-all.shCore verification generates JaCoCo coverage in target/site/jacoco/ (HTML, XML, and CSV). To also run OWASP Dependency-Check against runtime and test dependencies:
mvn clean verify -PsecurityThe security profile writes HTML and JSON reports to target/dependency-check/, uses the official NVD JSON feeds, and fails the build for findings with CVSS 7.0 or higher. The first scan downloads the local vulnerability database; later scans reuse it.
For release dry-run verification of packaged artifacts and consumer examples:
scripts/verify-release-dry-run.shFor examples only:
scripts/verify-examples.shVersion alignment across the root project and standalone adapters:
scripts/check-version-alignment.shDetailed verification evidence is maintained in docs/test-report.md. The implementation plan and status history live in PLAN.md, not at the top of this README.
Start here:
examples/README.md— standalone consumer examples.examples/maven-basic/— Maven plugin adoption.examples/gradle-basic/— Gradle plugin adoption.examples/junit-platform-basic/— JUnit Platform adoption.examples/bytecode-doubles-basic/— optional non-final concrete-class doubles.examples/bytecode-agent-basic/— optional final-class and static-method doubles.docs/usermanual/Home.md— user manual with more CLI details.docs/release-notes-1.0.0.md— 1.0 RC release notes.docs/compatibility-policy-1.0.md,docs/java-compatibility-1.0.md, anddocs/troubleshooting.md— 1.0 policy, Java matrix, and diagnostics.docs/migration-guide-1.0.md,docs/junit-to-javaspec-guide.md, anddocs/cucumber-boundary.md— migration and boundary guides.javaspec-gradle-plugin/README.md— Gradle plugin details.javaspec-junit-platform-engine/README.md— JUnit Platform engine details.CHANGELOG.md— release-change log scaffold.RELEASING.md— release-readiness checklist and publication blockers.
Architecture and decisions:
PLAN.md— implementation plan and requirement traceability.docs/arc42/— architecture documentation.docs/arc42/01-introduction-and-goals.mddocs/arc42/02-constraints.mddocs/arc42/03-context-and-scope.mddocs/arc42/04-solution-strategy.mddocs/arc42/05-building-block-view.mddocs/arc42/06-runtime-view.mddocs/arc42/07-deployment-view.mddocs/arc42/08-concepts.mddocs/arc42/09-architecture-decisions.mddocs/arc42/10-quality-requirements.mddocs/arc42/11-risks-and-technical-debt.mddocs/arc42/12-glossary.mddocs/adr/— architectural decision records.docs/research/phpspec-feature-inventory.md— phpspec feature inventory.docs/research/java-lts-data-structures.md— Java LTS profile research.
