Verifying your application
The contract states what Parsley expects of an application; this page is how the mechanically checkable clauses run in your test suite instead of being remembered. The docs describe, the probes verify. Nothing here replaces the runtime's fail-closed checks — probes move discovery from deploy time to test time.
Test-time probes
ContractProbes.probe drives your application's own testTopology() under
TopologyTestDriver — broker-less, so it runs in plain CI — with sample records you supply,
at least one per source topic. kafka-streams-test-utils must be on the test classpath (it
is an optional dependency of Parsley, so your test scope declares it — the coordinates are in
getting started — and production classpaths never
see it).
@Test
void applicationMeetsTheContract(@TempDir Path stateDir) {
ContractProbes.probe(
Parsley.named("settlements-app", settlement, balances),
ContractProbes.Samples.of()
.on(orders, "o-1", sampleOrder())
.on(payments, "p-1", samplePayment()),
stateDir
).assertOk();
}
assertOk() throws with every failure at once, each finding naming the probe, what it
observed, and the contract clause it checks. A finding names its clause as the Javadoc element
that states it, so the pointer stays valid however this site is arranged:
| Probe | Checks | Clause |
|---|---|---|
key-codec |
Key codecs are deterministic and canonical on the samples: encode twice agrees, decode-then-encode returns the same bytes. The encoded bytes choose the partition and key the fold state. | Topic#of |
stamps |
Every record observed on a sink carries parseable vc, vc-sender, vc-seq headers. |
CausalClock |
sink-decode |
Every observed output decodes with its sink topic's own codecs — the bytes the next hop's consumer will decode. | Codec |
declared-sinks |
No handler path emits to an undeclared sink. | Handler |
logic |
Handlers and folds are total on their samples — a throwing handler is a finding, not a stack trace to reverse-engineer. | Handler |
samples |
Every sample names a topic some stage consumes — a misconfigured probe run fails, it does not silently pass. | Parsley |
topic-exists |
Every declared source, sink and tick topic resolves on the cluster. | Parsley |
co-partition |
A stage's sources agree on partition count. | Stage |
tick-topic |
A stage's tick topic carries exactly the partition count of its widest source. | Stage.Builder#ticks |
names |
The store names each stage pins, which must stay stable across deployments. | Parsley#named |
Findings that are gaps rather than violations arrive as notes: a source topic with no sample (its handler path went unexercised), a declared sink no sample reached, the tick topics the deployment must create, and the store names each stage pins — the reminder that stage names and the application id stay stable across deployments. A report with notes and no failures passed, with stated blind spots.
Ticking stages tick under the probe: the driver's wall clock advances past the widest declared interval, and the emitted ticks pass through the same stamp checks as every other record.
Cluster probes
The clauses about the cluster itself — topics exist before start, a stage's sources agree on
partition count (the mechanical half of co-partitioning;
the key-bytes half is yours), tick topics carry exactly the partition count of the stage's
widest source — cannot be seen by a test driver. ContractProbes.probeCluster asks a live
cluster, read-only, without starting the application:
try (Admin admin = Admin.create(Map.of("bootstrap.servers", brokers))) {
ContractProbes.probeCluster(
Parsley.named("settlements-app", settlement, balances), admin).assertOk();
}
Run it from a deploy pipeline against the target environment; what it checks is exactly what
Parsley.streams will fail closed on at init, minus the surprise.
What probes cannot check
- Co-partitioning's key half. That related keys land on the same partition number of every source is a property of your producers' keys and encodings; the probe checks the partition-count half only.
- Plain producers outside the topology. A producer that should stamp with a
CausalClockbut does not is invisible here — the probe sees only the topology's own outputs. An unstamped record claims nothing; safe for itself, invisible to downstream ordering. - Purity and closed effects. A handler that writes to an external system looks pure to every black-box check; the Logic clause stays yours to uphold.
- The causal guarantee itself. That is Parsley's obligation, discharged by its own verification, not something each application re-proves.