This manual explains how to use Observer's Java-facing provider library.
It is written for projects that want Java-native test authoring while still exposing deterministic provider contracts to Observer.
cd lib/java
make test
make example-smoke
cd starter
make list
make verifyThat gives you the whole path in order:
- validate the library itself
- see the smallest executable example
- build a real provider host
- inspect raw provider discovery output
- verify canonical inventory, suite hash, and JSONL report artifacts
If your application already owns its CLI, use starter-embedded/ instead. That path keeps normal application behavior outside an explicit observe routing point.
If you want a normal consumer-shaped Java app rather than an SDK-local example, see ../../examples/java-consumer/.
The SDK now includes native Java project metadata too:
pom.xmlfor Maven usersbuild.gradleandsettings.gradlefor Gradle usersmodule-info.javafor JPMS-aware projects
Those metadata files now also carry publish-ready release information:
- MIT license declaration for the library subtree
- GitHub SCM and issue tracker links
- source and Javadoc jar generation
- optional signing hooks for release publication
This library is the Java-facing micro-SDK for Observer's provider contract.
It does not try to replace JUnit or become a general Java test framework.
It gives Java projects a deterministic way to:
- author Observer-exposed tests in Java
- assign stable identities explicitly when needed
- emit bounded observational data
- expose a standard provider host contract to the Observer CLI
Use static imports from Observer:
import static io.frogfish.observer.Observer.*;
var tests = collectTests(() -> {
describe("pkg", () -> {
test("smoke", ctx -> {
ctx.stdout("ok\n");
expect(true).toBeTruthy();
});
});
});That shape is deliberate.
- Java authors keep lambdas and explicit block structure
- the library surface stays small
- the deterministic Observer contract stays visible
When you omit id(...), Observer derives an identity from:
- suite path segments in order
- the test title
- duplicate occurrence order when the same derived name appears twice
That means:
describe("pkg")+test("smoke")becomespkg :: smoke- a second authored
pkg :: smokebecomespkg :: smoke #2
If a test needs a refactor-stable exported identity, set it explicitly:
test("smoke", ctx -> {
expect(true).toBeTruthy();
}, id("pkg::smoke"));Each test receives a TestContext.
That context owns:
stdout(...)stderr(...)setExit(...)fail(...)observe()
Example:
test("rejects overdraft", ctx -> {
ctx.stdout("denied overdraft\n");
expect(false).toBe(false);
});The expectation API is intentionally small:
expect(value).toBe(...)expect(value).toEqual(...)expect(value).toContain(...)expect(value).toMatch(...)expect(value).toBeTruthy()expect(value).toBeFalsy()expect(value).not().toBe(...)
Failed expectations raise ExpectationException, and the runtime host path converts that into a failing test result automatically.
Observation is opt-in and bounded.
Use ctx.observe() only when a test has structured data worth recording:
test("profile", ctx -> {
var observe = ctx.observe();
observe.metric("wall_time_ns", 104233.0);
observe.vector("latency_ns", java.util.List.of(1000.0, 1100.0, 980.0));
observe.tag("fixture", "fixtures/demo.json");
});The library enforces the same bounded telemetry rules as the other SDKs.
The Java library owns the standard provider host transport.
That means the host surface is:
listrun --target <target> --timeout-ms <u32>observe --target <target> --timeout-ms <u32>
The public SDK entrypoints are:
hostMain(...)hostDispatch(...)hostDispatchEmbedded(...)
The common direct-host shape is:
import static io.frogfish.observer.Observer.*;
public final class Host {
public static void main(String[] args) throws Exception {
var tests = collectTests(() -> {
describe("pkg", () -> test("smoke", ctx -> {
ctx.stdout("ok\n");
expect(true).toBeTruthy();
}, id("pkg::smoke")));
});
hostMain("java", tests, args);
}
}If a Java application already owns its CLI, it can expose an embedded observe namespace:
if (args.length > 0 && args[0].equals("observe")) {
hostDispatchEmbedded("java", "observe", tests, args);
return;
}
runNormalApplication(args);That keeps the app's normal UX intact while still exposing the standardized Observer host path.
The standalone starter lives in starter/.
It shows the full path:
- ordinary Java code under test
- Observer-authored tests in Java
- a tiny Java provider host wrapper script in
build/ - raw
listoutput - canonical inventory derivation
- suite execution
- canonical hash and JSONL verification
The useful commands are:
cd lib/java/starter
make list
make inventory
make host-run TARGET='ledger/rejects-overdraft'
make run
make report
make verifyThe app-shaped starter lives in starter-embedded/.
That path keeps an application-owned main(...) and routes observe ... through it.
Use it when the Java application should stay the product entrypoint.
The repo validates the Java SDK with plain javac and java so the surface stays toolchain-light inside this repository.
Use:
cd lib/java
make testThat compiles the library, runs the stdlib-only self-test, and checks list plus observe transport output.
If you are moving this into a conventional Java build, the matching native entrypoints are:
cd lib/java
mvn test
gradle checkFor local packaging without external build tools:
cd lib/java
make jar
make javadocIf your team already has a body of JUnit 5 tests, use ../java-junit5/ as the adaptation layer.
The bridge is intentionally narrow instead of magical.
- It collects ordinary zero-argument
@Testmethods. - It runs
@BeforeEachand@AfterEacharound each exported method target. - It derives exported Observer ids from
fully.qualified.ClassName#methodNameunless you supply@ObserverId. - It treats
@DisplayNameas authored title sugar, not as the exported deterministic contract.
That keeps the Java adoption path natural without weakening Observer's determinism rules.
The consumer-shaped examples for that path live outside the SDK subtree in:
../../examples/java-consumer-maven/../../examples/java-consumer-gradle/
For Java teams, the important DX improvement is that this SDK no longer forces a custom-only build story.
- Maven metadata already defines a Java 17 jar build, source jar, Javadoc jar, and self-test execution.
- Gradle metadata already defines a Java library build and wires
checkto the stdlib self-test. - The public package is exported through JPMS so modular applications can consume it cleanly.
The repo still validates with make because Maven and Gradle are optional here, not because Java consumers should have to avoid them.