What landed
Topic assertions
Use these when the outcome you care about is what landed on a topic, not one specific consumed record.
For field-by-field checks on one record, use Record assertions. For collection-style checks across many records, use Extraction assertions.
Entry point
Start with KafkaAssertions.assertThatTopic(topic) when you use @EnableWiretapKafkaBroker. Use the broker or bootstrap-server overloads only when you want to be explicit.
import static io.typecheck.wiretap.core.assertions.KafkaAssertions.assertThatTopic;
orderService.createOrder("Widget", 200);
assertThatTopic("orders-topic")
.exists()
.hasRecordCount(1);Listener readiness
Use readiness checks on input topics before publishing to a listener-driven flow. They wait for matching @KafkaListener containers to be running and assigned, but existing record-count and payload assertions do not wait implicitly.
| Property | Default | Description |
|---|---|---|
hasReadyListeners() | — | waits for all listeners subscribed to this topic |
hasReadyListeners(Duration) | — | same check with a custom timeout |
hasReadyListeners(ids...) | — | waits for named listeners and verifies each subscribes to this topic |
hasReadyListeners(Duration, ids...) | — | named listener checks with a custom timeout |
hasReadyListener(id) | — | waits for one listener and verifies it subscribes to this topic |
hasReadyListener(id, Duration) | — | named listener check with a custom timeout |
assertThatTopic("orders.in")
.hasReadyListeners();
publisher.send("orders.in", "ord-1", order);
assertThatTopic("orders.out")
.containsRecordWithKey("ord-1");assertThatTopic("orders.in")
.hasReadyListeners("orderListener", "auditListener")
.hasRecordCount(0);Existence and topology
These assertions are payload-agnostic, so they do not need typed overloads.
| Property | Default | Description |
|---|---|---|
exists() | — | proving a topic was created |
doesNotExist() | — | negative setup checks |
hasPartitionCount(int) | — | verifying topic shape |
hasRecordCount(int) | — | exact publication count |
hasRecordCountAtLeast(int) | — | background-activity-tolerant checks |
isEmpty() | — | negative-path publication checks |
Matcher-based scans
Keys
assertThatTopic("orders-topic").containsRecordWithKey("order-123");assertThatTopic("orders-topic")
.containsRecordWithKeyMatching(startsWith("order-"));String payloads
assertThatTopic("orders-topic").containsRecordWithValue("Widget");Typed payloads (JSON, Avro & Protobuf)
Use the typed overloads when the topic contains DTOs, Protobuf Messages, Avro GenericRecord, or Avro SpecificRecord values and you want the library to deserialize them for you. JSON DTOs are compared structurally, so they don't need equals().
OrderEvent expected = new OrderEvent("order-123", "customer-1", "Widget", 2, price, timestamp);
assertThatTopic("orders-topic")
.containsRecordWithValue(expected);assertThatTopic("orders-topic")
.containsRecordWithValue(OrderEvent.class, expected);assertThatTopic("orders-topic")
.hasRecordCount(1)
.containsRecordWithValueMatching(OrderEvent.class,
hasProperty("quantity", equalTo(2)));Reusing record assertions at topic scope
| Property | Default | Description |
|---|---|---|
containsRecordSatisfying(...) | — | at least one matching record |
allRecordsSatisfy(...) | — | every record must satisfy the same contract |
noRecordSatisfies(...) | — | no record may satisfy a forbidden condition |
assertThatTopic("orders-topic")
.containsRecordSatisfying(record -> record
.hasKey("order-123")
.hasValueMatching(containsString("Widget")));assertThatTopic("orders-topic")
.allRecordsSatisfy(OrderEvent.class, record -> record
.hasJsonPath("$.status", "CREATED")
.hasValueMatching(hasProperty("product", containsString("Widget"))));Advanced compositions
Common contract plus selected records
Combine a count with allRecordsSatisfy(...) when every event must share a contract, then add containsRecordSatisfying(...) checks for specific cases. Include a count when records must exist, because allRecordsSatisfy(...) passes for an empty topic.
assertThatTopic("orders")
.hasRecordCount(2)
.allRecordsSatisfy(OrderEvent.class, record -> record
.hasJsonPath("$.customerId", "cust-7")
.hasHeader("X-Correlation-Id"))
.containsRecordSatisfying(OrderEvent.class, record -> record
.hasKey("order-123")
.hasJsonPath("$.status", "CREATED"))
.containsRecordSatisfying(OrderEvent.class, record -> record
.hasKey("order-124")
.hasJsonPath("$.status", "PAID"));Exact distinct records
Two containsRecordSatisfying(...) calls can be satisfied by the same record when their conditions overlap. Use records(...).inAnyOrder().containingExactly(...)when the topic must contain distinct records matching an exact set of expectations.
assertThatTopic("orders")
.records(OrderEvent.class)
.inAnyOrder()
.containingExactly(
record -> record
.hasJsonPath("$.customerId", "cust-7")
.hasKey("order-123")
.hasJsonPath("$.status", "CREATED"),
record -> record
.hasJsonPath("$.customerId", "cust-7")
.hasKey("order-124")
.hasJsonPath("$.status", "PAID"));Shared contract plus an exact set
When records share fields but differ in the business transition being asserted, check the common contract once and use exact-record matching for the varying fields.
assertThatTopic("orders")
.hasRecordCount(2)
.allRecordsSatisfy(OrderEvent.class, record -> record
.hasJsonPath("$.customerId", "cust-7"))
.records(OrderEvent.class)
.inAnyOrder()
.containingExactly(
record -> record.hasJsonPath("$.status", "CREATED"),
record -> record.hasJsonPath("$.status", "PAID"));Ordered records and snapshots
Use records() when the whole observed sequence matters. The typed overloads reuse the same JSON, Avro and Protobuf deserialization paths as the existing topic assertions, and failures include a table of observed records.
| Property | Default | Description |
|---|---|---|
records() | — | starts an ordered assertion chain for String payloads |
records(Class) | — | starts an ordered assertion chain for JSON DTO, Avro or Protobuf payloads |
inOrder() | — | requires expectations to match topic read order |
inAnyOrder() | — | allows exact expectations to match in any order |
ignoringJsonPaths(...) | — | deletes volatile fields before snapshot comparison |
containsExactlyJson(...) | — | compares normalized JSON payload snapshots |
assertThatTopic("orders-topic")
.records(OrderEvent.class)
.inOrder()
.containingExactly(
record -> record.hasJsonPath("$.status", "CREATED"),
record -> record.hasJsonPath("$.status", "PAID"),
record -> record.hasJsonPath("$.status", "SHIPPED"));assertThatTopic("orders-topic")
.records(OrderEvent.class)
.ignoringJsonPaths("$.createdAt", "$.traceId")
.containsExactlyJson(expectedJson1, expectedJson2);When to move to extraction assertions
Prefer Extraction assertions when the test really wants to say:
- "these are the keys that were published"
- "these are the payload field values that were published"
- "all extracted values should satisfy a collection-style assertion"