Resource library

QA How-To

Event-Driven API Testing Complete Guide (2026)

Event driven API testing complete guide for contracts, Kafka flows, retries, dead-letter queues, observability, and reliable CI automation for teams in 2026.

20 min read | 3,327 words

TL;DR

A reliable event-driven API test strategy checks schemas first, producer and consumer contracts next, real broker integration after that, and a small number of end-to-end flows last. Use correlation IDs, bounded polling, isolated topics, deterministic fixtures, and explicit assertions for retries, idempotency, ordering, and dead-letter behavior.

Key Takeaways

  • Test event contracts, broker behavior, consumer outcomes, and failure policies as separate layers.
  • Use a real ephemeral broker for integration tests instead of mocking every transport detail.
  • Correlate messages with unique test IDs and wait for observable outcomes instead of fixed sleeps.
  • Verify duplicate delivery, ordering assumptions, retries, and dead-letter records explicitly.
  • Enforce backward-compatible schemas in CI before producers publish breaking events.
  • Keep a small production-like end-to-end suite and a larger fast contract suite.

Event driven API testing complete guide searches usually begin with a hard truth: an HTTP 202 response proves only that a request was accepted. It does not prove that the event was valid, published once, consumed correctly, retried safely, or reflected in the final business state. You need evidence at every asynchronous boundary.

This guide gives you a layered strategy and a runnable TypeScript example using Kafka, KafkaJS, Vitest, Ajv, and Testcontainers. You will test the contract, publish through a real broker, observe a consumer side effect, and cover the failure paths that create most production incidents.

TL;DR

Layer What it proves Best test double Run frequency
Schema Event shape and compatibility No broker Every commit
Producer contract Correct topic, key, headers, and payload Capturing publisher Every commit
Consumer contract Handler behavior for valid and invalid events Direct handler call Every commit
Broker integration Serialization, routing, offsets, and real client configuration Ephemeral broker Pull request
Workflow Business result across services Production-like environment Release or focused CI

Build most coverage below the broker boundary. Use real infrastructure where client and broker behavior matters. Reserve full workflows for a few business-critical journeys. Never use fixed sleeps as proof of completion.

What You Will Build

You will create a small order event pipeline and its test harness. By the end, you will be able to:

  • Validate an order.created JSON event before it reaches Kafka.
  • Start an isolated Kafka-compatible broker for an integration test.
  • Publish an event with a stable key, correlation ID, and event ID.
  • Consume the event and verify an observable business side effect.
  • Test duplicates, malformed messages, retries, and dead-letter expectations.
  • Translate the local suite into a layered CI pipeline.

The example uses an in-memory projection as the observable side effect. In a service, replace that projection with a repository, API read model, outbound message capture, or database query. The test principle remains the same: assert what the consumer promises, not its private implementation.

Prerequisites

Use Node.js 22 or a supported newer LTS release, npm, and a Docker-compatible container runtime. Testcontainers discovers Docker, Podman, or another supported runtime through its normal configuration. Confirm the basics:

node --version
docker version
mkdir event-api-test && cd event-api-test
npm init -y
npm install kafkajs ajv ajv-formats
npm install --save-dev typescript vitest @testcontainers/kafka @types/node

Add scripts and module settings to package.json:

{
  "type": "module",
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest"
  }
}

Create src and test directories. The test starts its own broker, so you do not need a shared Kafka installation or fixed local port. This isolation is valuable in CI because parallel jobs cannot steal each other's messages.

Verification: Run npm test. Vitest should start successfully and report that no test files were found. If Docker is unavailable, fix that before continuing.

Step 1: Define the Event Contract for This Event Driven API Testing Complete Guide

Start with a versioned envelope. The envelope carries operational metadata while data holds the business payload. Create src/contract.ts:

import Ajv, { type JSONSchemaType } from "ajv";
import addFormats from "ajv-formats";

export interface OrderCreated {
  specVersion: "1.0";
  eventId: string;
  eventType: "order.created";
  occurredAt: string;
  correlationId: string;
  data: { orderId: string; customerId: string; totalCents: number };
}

export const orderCreatedSchema: JSONSchemaType<OrderCreated> = {
  type: "object",
  additionalProperties: false,
  required: ["specVersion", "eventId", "eventType", "occurredAt", "correlationId", "data"],
  properties: {
    specVersion: { type: "string", const: "1.0" },
    eventId: { type: "string", minLength: 1 },
    eventType: { type: "string", const: "order.created" },
    occurredAt: { type: "string", format: "date-time" },
    correlationId: { type: "string", minLength: 1 },
    data: {
      type: "object", additionalProperties: false,
      required: ["orderId", "customerId", "totalCents"],
      properties: {
        orderId: { type: "string", minLength: 1 },
        customerId: { type: "string", minLength: 1 },
        totalCents: { type: "integer", minimum: 0 }
      }
    }
  }
};

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
export const validateOrderCreated = ajv.compile(orderCreatedSchema);

A schema is executable documentation. additionalProperties: false catches accidental fields, while required envelope properties make tracing possible. Whether strict extra-field rejection is appropriate depends on your evolution policy. Consumers intended to tolerate additive changes can validate only the fields they own.

Add a fast contract test in test/contract.test.ts:

import { expect, test } from "vitest";
import { validateOrderCreated } from "../src/contract.js";

test("rejects an invalid order event", () => {
  const event = { eventType: "order.created", data: { totalCents: -1 } };
  expect(validateOrderCreated(event)).toBe(false);
  expect(validateOrderCreated.errors?.length).toBeGreaterThan(0);
});

Verification: Run npm test -- contract.test.ts. The test passes only when validation rejects the incomplete negative-value payload. Also add a valid fixture test, because a schema that rejects everything is equally broken.

For governed Avro contracts, follow the deeper guide to validate Avro schema compatibility in CI. The same principle applies: compatibility is a build gate, not a production discovery.

Step 2: Separate Producer and Consumer Contracts

Do not begin every test with Kafka. First isolate the code that decides what to publish and the code that handles a received event. Create src/orders.ts:

import { validateOrderCreated, type OrderCreated } from "./contract.js";

export interface Publisher {
  publish(topic: string, key: string, value: OrderCreated): Promise<void>;
}

export async function createOrder(
  input: OrderCreated["data"], metadata: Omit<OrderCreated, "data" | "eventType" | "specVersion">,
  publisher: Publisher
) {
  const event: OrderCreated = { specVersion: "1.0", eventType: "order.created", ...metadata, data: input };
  if (!validateOrderCreated(event)) throw new Error("Invalid order.created event");
  await publisher.publish("orders.created.v1", input.orderId, event);
  return event;
}

export class OrderProjection {
  private readonly orders = new Map<string, OrderCreated["data"]>();
  apply(event: OrderCreated) {
    if (!validateOrderCreated(event)) throw new Error("Invalid event");
    if (!this.orders.has(event.eventId)) this.orders.set(event.eventId, event.data);
  }
  hasEvent(eventId: string) { return this.orders.has(eventId); }
  get size() { return this.orders.size; }
}

The Publisher port lets a unit test capture the complete producer contract without emulating Kafka. The handler accepts a domain event, not a Kafka message object, so business rules can be tested independently from offsets and partitions.

A producer test should assert topic, key, payload, and required headers if headers belong to your interface. A consumer test should pass valid, boundary, duplicate, and invalid events directly to the handler. This division makes failures diagnostic: contract failures point to event construction, while integration failures point to wiring or broker behavior.

Verification: Use a publisher object whose publish method pushes arguments into an array. Call createOrder, then assert the topic is orders.created.v1, the key equals orderId, and the event contains the supplied correlation ID. Call projection.apply(event) twice and assert projection.size remains 1.

See Kafka consumer contract testing step by step for provider and consumer fixture patterns beyond this survey.

Step 3: Start a Real Broker with Testcontainers

Broker integration tests catch errors that mocks cannot: unreachable listeners, incorrect security settings, topic configuration, partition assignment, serialization, consumer group behavior, and offset commits. Create test/kafka.integration.test.ts with this setup:

import { afterAll, beforeAll, expect, test } from "vitest";
import { Kafka, type Consumer, type Producer } from "kafkajs";
import { KafkaContainer, type StartedKafkaContainer } from "@testcontainers/kafka";
import { OrderProjection, createOrder } from "../src/orders.js";
import type { OrderCreated } from "../src/contract.js";

let container: StartedKafkaContainer;
let producer: Producer;
let consumer: Consumer;
let broker: string;
const topic = `orders.created.v1.${Date.now()}`;

beforeAll(async () => {
  container = await new KafkaContainer("confluentinc/cp-kafka:8.0.0").start();
  broker = `${container.getHost()}:${container.getMappedPort(9093)}`;
  const kafka = new Kafka({ clientId: "order-test", brokers: [broker] });
  const admin = kafka.admin();
  await admin.connect();
  await admin.createTopics({ topics: [{ topic, numPartitions: 1, replicationFactor: 1 }] });
  await admin.disconnect();
  producer = kafka.producer();
  consumer = kafka.consumer({ groupId: `order-test-${Date.now()}` });
  await Promise.all([producer.connect(), consumer.connect()]);
}, 120_000);

afterAll(async () => {
  await Promise.allSettled([consumer?.disconnect(), producer?.disconnect()]);
  await container?.stop();
});

KafkaContainer configures an external listener whose advertised address matches the host-mapped port. KafkaJS therefore receives reachable broker metadata after its initial connection, instead of reconnecting to an internal or hard-coded port. Pin the image to a version your team has qualified, then update it deliberately. A unique topic and group isolate the test. One partition makes ordering assertions deterministic, but it must not become an excuse to assume global ordering in production.

Verification: Run npm test -- kafka.integration.test.ts. The container should start, KafkaJS should connect, the topic should be created, and the test file should reach its test cases. An image pull can make the first run slower; later runs normally reuse the local image.

Step 4: Publish, Consume, Correlate, and Assert

Add the following test below the setup. It subscribes before producing, waits until the consumer is assigned, and resolves when the target event produces the promised side effect:

test("projects an order created event", async () => {
  const projection = new OrderProjection();
  const eventId = crypto.randomUUID();
  const correlationId = crypto.randomUUID();
  let resolveSeen!: () => void;
  const seen = new Promise<void>((resolve) => { resolveSeen = resolve; });

  await consumer.subscribe({ topic, fromBeginning: true });
  const running = consumer.run({
    eachMessage: async ({ message }) => {
      const event = JSON.parse(message.value!.toString()) as OrderCreated;
      if (event.correlationId !== correlationId) return;
      projection.apply(event);
      resolveSeen();
    }
  });

  const publisher = {
    publish: async (_topic: string, key: string, value: OrderCreated) => {
      await producer.send({
        topic,
        messages: [{ key, value: JSON.stringify(value), headers: { correlationId } }]
      });
    }
  };

  await createOrder(
    { orderId: "order-101", customerId: "customer-7", totalCents: 2599 },
    { eventId, correlationId, occurredAt: new Date().toISOString() },
    publisher
  );

  await expect(Promise.race([
    seen,
    new Promise((_, reject) => setTimeout(() => reject(new Error("event not observed")), 10_000))
  ])).resolves.toBeUndefined();
  expect(projection.hasEvent(eventId)).toBe(true);
  await consumer.stop();
  await running;
}, 30_000);

The correlation ID prevents an unrelated message from satisfying the test. The event ID represents identity for deduplication. The order ID is the Kafka key, which keeps events for one order in the same partition. These identifiers serve different jobs and should not be collapsed into one value.

Promise.race creates a bounded wait rather than an arbitrary pause. A fixed five-second sleep is both slow when the result arrives immediately and flaky when CI needs slightly longer. In production-scale tests, use a reusable polling helper with a deadline, interval, and final diagnostic message.

Verification: The test passes only after the matching message is consumed and the projection contains its event ID. Temporarily change the topic in producer.send to confirm the timeout failure clearly reports that the event was not observed. Restore it afterward.

Step 5: Test Delivery Semantics, Ordering, and Idempotency

Kafka and most queue systems provide at-least-once delivery in common consumer designs. A handler can finish its side effect and fail before committing its offset, causing redelivery. Your test must prove that replay does not duplicate money movement, notifications, or records.

Send the same event twice and assert one business effect:

const duplicate: OrderCreated = {
  specVersion: "1.0", eventType: "order.created",
  eventId: "evt-duplicate-1", correlationId: "corr-duplicate-1",
  occurredAt: new Date().toISOString(),
  data: { orderId: "order-202", customerId: "customer-8", totalCents: 5000 }
};
projection.apply(duplicate);
projection.apply(duplicate);
expect(projection.size).toBe(1);

The sample projection stores by event ID. A real consumer usually writes the event ID to an inbox table in the same database transaction as the business update. A process-local Set is insufficient because it disappears on restart and cannot coordinate multiple replicas.

Ordering requires equally explicit tests. Kafka preserves order only within a partition. Publish events for the same aggregate with the same key, include an aggregate sequence number, and test the consumer's policy for gaps and old versions. It might reject stale events, buffer a short gap, or rebuild from the source. Do not test or promise global topic order.

Also test concurrency with different keys. A handler that works sequentially may expose lost updates when two partitions execute at once. Assert the durable result, then replay the messages after a consumer restart to verify offset and idempotency behavior together.

Verification: Run the duplicate test and confirm one durable effect. Then intentionally remove the event ID guard. The test should fail with a count of two, demonstrating that it detects the risk rather than merely exercising code.

Step 6: Verify Retries and the Dead-Letter Queue

Retry testing is a state transition test, not just an assertion that an error was logged. Define the expected policy before automating it:

Failure Retry? Expected route Assertion
Temporary timeout Yes, bounded Retry topic or delayed queue Attempt count and eventual success
Rate limit Yes, delayed Retry with backoff Delay metadata and no hot loop
Invalid schema No Dead-letter topic Original bytes and validation reason
Permanent business rejection Usually no Dead-letter or rejection event Stable reason code
Unknown exception Bounded Dead-letter after limit Final attempt count and trace context

Keep retry delays injectable so unit tests do not wait in real time. At integration level, configure short test-only delays but preserve the same attempt count and routing logic. Publish a poison event, consume the retry or dead-letter topic with a unique correlation ID, and assert the original payload plus diagnostic headers such as source topic, exception class, attempt count, and first-failure timestamp. Avoid sensitive stack traces in externally readable messages.

A useful test proves three things together: the main consumer does not acknowledge a transient failure as success, retry attempts stop at the configured limit, and the final record reaches the dead-letter topic exactly once from a business perspective. Then replay the dead-letter record after correcting the cause and verify that idempotency prevents duplicate effects.

Verification: Instrument the test handler with an attempt counter. Make it throw on attempts one and two, succeed on attempt three, and assert one business result. In a second case, always throw and assert the dead-letter message reports the configured final attempt. The focused dead-letter queue retry testing guide provides a complete harness.

Step 7: Add Observability Assertions

An asynchronous failure is expensive when a test says only expected true, received false. Carry the correlation ID through producer headers, structured logs, consumer spans, and outgoing events. Then make the test harness collect those artifacts on failure.

At minimum, assert that malformed input produces a stable reason code and that logs identify the event ID, event type, topic, partition, and correlation ID. Metrics should distinguish consumed, succeeded, retried, and dead-lettered outcomes. Traces should connect the initiating HTTP request to publication and consumption when your telemetry stack supports trace context propagation.

Do not make every integration test depend on a full observability platform. Unit-test the structured log or metric adapter, and keep a small environment test that queries the actual backend. This preserves fast feedback while still detecting broken exporters or missing context propagation.

Collect broker and container logs automatically when the test deadline expires. Print the topic, group ID, correlation ID, known offsets, and last observed event. These details turn a generic timeout into an actionable report. Redact tokens and personal data before attaching diagnostics to CI artifacts.

Verification: Force the handler to reject one event. Confirm the captured diagnostic contains the exact correlation ID and a machine-readable reason such as SCHEMA_INVALID. Search by that ID in your local output and ensure one failure path can be reconstructed without reading the source code.

Step 8: Build a Reliable Event Driven API Testing Complete Guide CI Pyramid

Split the suite by feedback speed and infrastructure cost. A practical pipeline runs schema and handler tests on every commit, broker integration tests on pull requests, and a few cross-service workflows after deployment to a controlled environment. Test selection should reflect risk, not a desire for a perfect pyramid shape.

Use unique topic prefixes per CI job and unique consumer groups per test. Put resource cleanup in afterAll, but also configure retention so abandoned topics disappear. Cap all waits. Run independent scenarios in parallel only when their broker resources and databases are isolated. Cache container images where your runner permits it, while continuing to pin and scan the qualified image.

Never share one long-lived staging consumer group between concurrent test runs. One job can consume another job's record and create a failure that looks random. Correlation filtering helps observation but does not prevent destructive competition for messages. Isolation at topic, namespace, virtual host, account, or broker level is stronger.

Treat test data as an API. Keep builders for valid baseline events, override only the field under test, and avoid copied fixtures that silently drift. Store golden serialized records only when byte-level compatibility matters. For RabbitMQ-based systems, the RabbitMQ event mocking integration tutorial explains where a fake channel is useful and where a real broker is essential.

Verification: Run the suite twice concurrently with different job prefixes. Both runs should pass, consume only their own records, and clean up without waiting for the other. Repeat one integration test several times locally to expose lifecycle leaks before CI does.

Choosing the Right Test Boundary

A common mistake is choosing between mocks and real brokers as if only one can be correct. Use the smallest boundary that can disprove the risk. A producer unit test can prove routing decisions. It cannot prove authentication or broker listeners. A container test can prove serialization and consumption. It does not automatically prove that two deployed teams agree on semantic meaning.

Contract tests should include meaning, not only shape. totalCents must state its currency relationship, valid range, and whether refunds are negative or represented by another event. Timestamps must specify whether they describe occurrence, publication, or processing. Keys, nullability, default values, and compatibility policy belong in the contract review.

For a full workflow, begin from an externally supported interface and observe a supported outcome. For example, submit an order over HTTP, receive 202, then poll an order-status API until the projection becomes CONFIRMED. Assert intermediate events only when they are part of a team contract or needed to diagnose the workflow. Testing every internal hop couples the suite to orchestration details and makes safe refactoring painful.

For broader request-response coverage, pair this strategy with an API contract testing guide and API negative testing techniques. Event-driven and synchronous interfaces share validation concerns, but their completion and failure evidence differ.

The Complete Series

Use this pillar as the map, then implement each high-risk area with the focused tutorials:

Troubleshooting

Consumer receives no messages -> Subscribe and start consumer.run before publishing. Use a unique group, verify the exact topic, inspect assignment logs, and check whether another consumer in the same group owns the partition.

Container starts but the client cannot connect -> Use the dedicated @testcontainers/kafka module and connect to ${container.getHost()}:${container.getMappedPort(9093)}. The module configures Kafka to advertise that external listener, so metadata reconnects use the reachable mapped address. Do not replace it with a hard-coded localhost:9092 listener.

Tests pass locally but time out in CI -> Remove fixed sleeps, increase the bounded deadline to match cold image startup, capture container logs, and verify runner CPU and Docker availability. Do not hide a lifecycle leak with unlimited retries.

Old messages satisfy a new test -> Create a unique topic or consumer group and filter observations by correlation ID. fromBeginning: false alone is not sufficient when timing and committed offsets vary.

Duplicate effects appear during retries -> Persist processed event IDs transactionally with the business change. Do not rely on process memory or assume exactly-once business execution from a broker marketing term.

Schema tests pass but consumers break -> Add compatibility checks against prior registered versions and consumer-owned semantic fixtures. Shape validation cannot detect a changed unit, meaning, key strategy, or enum interpretation.

Common Mistakes and Best Practices

Avoid these recurring errors:

  • Asserting publication only and calling the workflow tested. Observe the consumer's promised outcome.
  • Mocking the entire broker client in every test. Keep mocks for decisions, then use a real broker for wiring.
  • Using sleeps instead of condition-based waits with deadlines and diagnostics.
  • Reusing topics and groups across parallel jobs. Give every run an isolated namespace.
  • Assuming exactly-once delivery means exactly-once business effects. Design and test idempotency.
  • Testing only the happy payload. Include malformed, unknown-version, stale, duplicate, out-of-order, and poison events.
  • Checking JSON shape but ignoring semantic compatibility. Document units, meaning, defaults, and ownership.
  • Retrying permanent failures. Classify errors and route poison records without hot loops.

Prefer deterministic builders, stable reason codes, explicit time control, transactional inbox or outbox patterns, and diagnostics keyed by correlation ID. Keep the majority of tests fast. A small number of realistic integration tests should protect the broker boundary, not dominate the suite.

Where To Go Next

Start by inventorying every event your service produces and consumes. For each event, record the owner, topic or exchange, key strategy, schema version, compatibility policy, retry limit, dead-letter destination, and observable business outcome. That table quickly reveals missing contracts and untested failure paths.

Then add one valid fixture and one invalid fixture per consumed event. Add producer capture tests. Select the highest-risk workflow for a real broker integration test like the one in this guide. Finally, choose the relevant deep dive from the complete event testing series and automate schema compatibility and dead-letter replay in CI.

Interview Questions and Answers

Q: How do you test an event-driven API?

Test it in layers: schema validation, producer contract, consumer handler, broker integration, and a small end-to-end workflow. Correlate the published event with the observed business result and use bounded polling instead of sleeps. Add explicit cases for duplicates, ordering, retries, and dead-letter routing.

Q: Why is an HTTP 202 response insufficient?

It confirms acceptance, not asynchronous completion. Publication, consumption, downstream validation, and persistence can still fail. The test must observe a durable state, output event, or other supported business outcome.

Q: When should you use a real Kafka broker?

Use one when the risk involves serialization, listeners, authentication, topics, partitions, offsets, groups, or client configuration. Use direct handler tests for business logic and a capturing publisher for routing decisions. This combination gives useful fidelity without making every test slow.

Q: How do you prevent flaky asynchronous tests?

Isolate topics and groups, use unique correlation IDs, start consumers before publishing, and wait for a condition with a deadline. Capture offsets and broker logs when the deadline expires. Never depend on a fixed delay or message created by another test.

Q: How do you test at-least-once delivery?

Deliver the identical event more than once and assert one durable business effect. Restart or fail the consumer around offset commit boundaries where practical. Persist an event ID in the same transaction as the business update so replay remains safe.

Q: What belongs in a dead-letter message?

Keep the original payload or a safe reference, source destination, event and correlation IDs, attempt count, timestamps, and a stable failure reason. Protect sensitive data and avoid leaking secrets in stack traces. Test that replay after remediation is idempotent.

Conclusion

A strong event driven API testing complete guide must go beyond publishing a message. The reliable strategy validates contracts early, isolates producer and consumer logic, exercises the real broker where transport behavior matters, and proves business outcomes under duplicates and failures.

Begin with one critical event. Give it a versioned contract, event ID, correlation ID, deterministic fixture, consumer contract test, and isolated broker test. Then automate compatibility, retry, dead-letter, and observability checks until asynchronous failures become clear test results instead of production mysteries.

Interview Questions and Answers

How would you design an event-driven API test strategy?

I would layer schema checks, producer contract tests, direct consumer-handler tests, real broker integration tests, and a few end-to-end workflows. I would prioritize negative cases such as malformed events, duplicates, reordering, retries, and poison messages. Each asynchronous test would use isolated resources, correlation IDs, bounded waits, and an assertion on a durable business outcome.

Why should event-driven tests avoid fixed sleeps?

A sleep waits the full duration even when processing is fast and still fails when a slow runner exceeds the guess. Condition-based polling or a completion signal ends as soon as the outcome appears and fails at a clear deadline. It can also report the last observed state, offsets, and correlation ID.

What should a Kafka consumer contract test verify?

It should verify the fields and semantics the consumer actually depends on, including types, required values, units, enums, and key assumptions. It should cover a valid baseline plus missing, invalid, duplicate, unknown-version, and boundary cases. Broker-specific behavior belongs in a separate integration layer.

How do you test message ordering correctly?

I first document the actual guarantee, such as ordering only for records with the same key in one partition. I publish sequenced events with the same aggregate key and verify the consumer policy for stale records and gaps. I do not assert global topic ordering unless the architecture truly provides it.

How do you prove a consumer is idempotent?

I deliver the same event ID multiple times and assert one durable business effect. For stronger coverage, I restart or fail the consumer near the offset commit boundary and replay the record. The implementation should persist deduplication state transactionally with the business update.

When would you mock RabbitMQ or Kafka?

I mock a narrow publishing or handler port when testing business decisions, payload construction, and error mapping. I use a real ephemeral broker when routing, acknowledgments, redelivery, partitions, exchanges, bindings, offsets, or client configuration are the risk. I avoid mocks that attempt to reimplement the broker.

What diagnostics would you capture for a timed-out event test?

I would capture the correlation and event IDs, topic, partition, consumer group, known offsets, last observed record, structured application logs, and broker or container logs. I would also report the expected business condition and its last actual value. All artifacts should redact secrets and sensitive payload data.

Frequently Asked Questions

What is event-driven API testing?

Event-driven API testing verifies producers, event contracts, brokers, consumers, and asynchronous business outcomes. It also checks delivery behavior such as duplicates, ordering, retries, and dead-letter routing.

Can I test Kafka without running Kafka?

Yes, direct handler tests and capturing publisher fakes cover much of the contract quickly. Use an ephemeral real broker for behavior involving topics, partitions, offsets, consumer groups, serialization, security, or client configuration.

How do I wait for an asynchronous event in a test?

Poll an observable condition or resolve a promise when a uniquely correlated event arrives. Always set a deadline and include useful diagnostics, rather than using an unconditional sleep.

How should event-driven tests run in CI?

Run schema and handler tests on every commit, broker integration tests on pull requests, and a small set of cross-service flows in a controlled environment. Isolate each job with unique topics, groups, or namespaces.

What is the difference between an event ID and a correlation ID?

An event ID uniquely identifies one event and supports deduplication. A correlation ID connects related work across a request or workflow and supports tracing; multiple events can share it.

How do I test dead-letter queue behavior?

Publish a deterministic poison event, observe each bounded retry, and assert that the final dead-letter record contains the original message and stable diagnostic metadata. Then test safe replay after the underlying problem is fixed.

Does exactly-once Kafka processing remove the need for idempotency tests?

No. Broker transactions do not automatically make every external database, payment, email, or HTTP side effect execute exactly once. Test duplicate delivery and enforce business-level idempotency at the side-effect boundary.

Related Guides