QA How-To
How to Test Event Driven Microservices (2026)
Learn how to test event driven microservices with contract, component, integration, and end-to-end checks for reliable asynchronous systems in modern CI.
22 min read | 2,533 words
TL;DR
Test event-driven microservices in layers: validate the event contract, test producers and consumers in isolation, verify broker behavior with a containerized dependency, and keep only a few end-to-end journeys. Assert observable business state with correlation IDs and bounded polling, never fixed delays.
Key Takeaways
- Test schemas, producer behavior, and consumer behavior independently before involving a real broker.
- Use correlation IDs and observable state instead of fixed sleeps when asserting asynchronous outcomes.
- Run most checks as fast contract and component tests, then reserve broker integration tests for transport semantics.
- Verify duplicates, reordering, poison messages, retry exhaustion, and dead-letter handling as first-class cases.
- Make tests deterministic with unique topics, isolated consumer groups, bounded polling, and controlled clocks.
- Keep a small end-to-end suite that proves one business journey across real deployed services.
To learn how to test event driven microservices, split the problem into contracts, service behavior, broker integration, and a small number of end-to-end workflows. That separation finds schema and business bugs quickly while still proving that Kafka, RabbitMQ, or another broker is configured correctly.
Asynchronous systems add failure modes that synchronous API tests rarely cover: duplicate delivery, reordered messages, consumer lag, poison events, retry storms, and eventual consistency. This tutorial builds a small order workflow in TypeScript with Vitest and Testcontainers, then exercises each risk deliberately. For broader background, read the event-driven API testing guide.
TL;DR
| Layer | Dependency | Best at finding | Typical speed | Run when |
|---|---|---|---|---|
| Schema or contract | JSON Schema or AsyncAPI document | Missing fields, incompatible types, breaking evolution | Milliseconds | Every commit |
| Producer component | Service plus mocked publisher | Wrong topic, key, headers, or event data | Milliseconds to seconds | Every commit |
| Consumer component | Handler invoked with an event | Business rules, idempotency, error classification | Milliseconds to seconds | Every commit |
| Broker integration | Service plus Kafka or RabbitMQ container | Serialization, routing, offsets, acknowledgments | Seconds | Pull request and CI |
| End to end | Multiple deployed services and real infrastructure | Wiring, permissions, configuration, full journey | Minutes | Release candidate |
Use all five layers, but do not give them equal weight. Put most scenarios in contract and component tests, a focused set in broker integration tests, and only critical journeys in end-to-end tests.
What You Will Build
You will test an order-created event that causes an inventory consumer to reserve stock. By the end, you will have:
- A versioned JSON Schema that rejects malformed events.
- A producer test that verifies topic, partition key, headers, and payload.
- An idempotent consumer test that proves duplicate delivery does not reserve twice.
- A Kafka integration test using a disposable container and a unique topic.
- A bounded eventual-consistency assertion with useful timeout diagnostics.
- Negative checks for poison messages, retries, and dead-letter routing.
The examples use a plain handler boundary so the same business tests survive a migration between brokers. Only the transport-focused test knows that Kafka is involved.
Prerequisites
Install Node.js 20 or newer, Docker, and npm. Create a TypeScript project, then install the runner, schema validator, Kafka client, and Testcontainers module:
npm init -y
npm install kafkajs ajv
npm install --save-dev typescript vitest @types/node testcontainers @testcontainers/kafka
npx tsc --init
Add "test": "vitest run" to the scripts object in package.json. Docker must be running because the integration step starts a real Kafka-compatible container. Verify the toolchain before writing application code:
node --version
docker version --format '{{.Server.Version}}'
npx vitest --version
Each command should print a version. If Docker reports that it cannot connect to the daemon, start Docker Desktop or your container runtime before continuing. Keep secrets out of test files. Local containers need no production credentials.
Step 1: Define and Validate the Event Contract
Start with a stable envelope. It gives operations and tests consistent metadata even as business payloads evolve. Save this as src/orderCreatedSchema.ts:
import type { JSONSchemaType } from 'ajv';
export interface OrderCreated {
eventId: string;
eventType: 'order.created';
schemaVersion: 1;
occurredAt: string;
correlationId: string;
data: { orderId: string; sku: string; quantity: number };
}
export const orderCreatedSchema: JSONSchemaType<OrderCreated> = {
type: 'object',
additionalProperties: false,
required: ['eventId', 'eventType', 'schemaVersion', 'occurredAt', 'correlationId', 'data'],
properties: {
eventId: { type: 'string', minLength: 1 },
eventType: { type: 'string', const: 'order.created' },
schemaVersion: { type: 'integer', const: 1 },
occurredAt: { type: 'string', format: 'date-time' },
correlationId: { type: 'string', minLength: 1 },
data: {
type: 'object',
additionalProperties: false,
required: ['orderId', 'sku', 'quantity'],
properties: {
orderId: { type: 'string', minLength: 1 },
sku: { type: 'string', minLength: 1 },
quantity: { type: 'integer', minimum: 1 }
}
}
}
};
Test both acceptance and rejection. Register ajv-formats if you want strict date-time format validation; here, disable format validation to keep the dependency list small and test the envelope shape:
import Ajv from 'ajv';
import { describe, expect, it } from 'vitest';
import { orderCreatedSchema } from './orderCreatedSchema';
const validate = new Ajv({ formats: { 'date-time': true } }).compile(orderCreatedSchema);
const validEvent = {
eventId: 'evt-101', eventType: 'order.created', schemaVersion: 1,
occurredAt: '2026-08-03T10:00:00.000Z', correlationId: 'trace-101',
data: { orderId: 'ord-101', sku: 'SKU-7', quantity: 2 }
};
describe('order.created contract', () => {
it('accepts the published shape', () => expect(validate(validEvent)).toBe(true));
it('rejects a zero quantity', () => {
expect(validate({ ...validEvent, data: { ...validEvent.data, quantity: 0 } })).toBe(false);
expect(validate.errors?.[0]?.keyword).toBe('minimum');
});
});
Run npx vitest run src/orderCreatedSchema.test.ts. Expect two passing tests. For cross-team ownership and compatibility policies, extend this approach with the contract testing guide or a schema registry compatibility check.
Step 2: Test the Producer Without a Broker
Make publishing an injected interface. The order service can then be tested without sockets, partitions, or background polling:
import type { OrderCreated } from './orderCreatedSchema';
export interface Publisher {
send(input: { topic: string; key: string; headers: Record<string, string>; value: OrderCreated }): Promise<void>;
}
export async function createOrder(
input: { orderId: string; sku: string; quantity: number },
publisher: Publisher,
ids: { eventId: string; correlationId: string },
now: () => Date
): Promise<void> {
const event: OrderCreated = {
eventId: ids.eventId, eventType: 'order.created', schemaVersion: 1,
occurredAt: now().toISOString(), correlationId: ids.correlationId, data: input
};
await publisher.send({
topic: 'orders.events', key: input.orderId,
headers: { 'event-type': event.eventType, 'correlation-id': ids.correlationId },
value: event
});
}
The partition key matters. Events for one order should land in the same Kafka partition when ordering within an aggregate is required. Assert the complete publish command, not merely that send was called:
import { expect, it, vi } from 'vitest';
import { createOrder } from './createOrder';
it('publishes a traceable order event keyed by order ID', async () => {
const send = vi.fn().mockResolvedValue(undefined);
await createOrder(
{ orderId: 'ord-9', sku: 'SKU-2', quantity: 3 }, { send },
{ eventId: 'evt-9', correlationId: 'corr-9' },
() => new Date('2026-08-03T12:00:00.000Z')
);
expect(send).toHaveBeenCalledWith(expect.objectContaining({
topic: 'orders.events', key: 'ord-9',
headers: { 'event-type': 'order.created', 'correlation-id': 'corr-9' },
value: expect.objectContaining({ eventId: 'evt-9', schemaVersion: 1 })
}));
});
Run the file with Vitest and expect one pass. A failure should show exactly which routing field drifted. This test cannot prove Kafka accepts the message, which is intentional; Step 4 covers transport.
Step 3: Test Consumer Logic and Idempotency
At-least-once delivery means the same event may reach a consumer more than once. Make the idempotency decision part of the consumer transaction boundary. The in-memory repository below demonstrates the contract clearly:
import type { OrderCreated } from './orderCreatedSchema';
export interface InventoryRepository {
hasProcessed(eventId: string): Promise<boolean>;
reserve(sku: string, quantity: number): Promise<void>;
markProcessed(eventId: string): Promise<void>;
}
export async function handleOrderCreated(event: OrderCreated, repo: InventoryRepository) {
if (await repo.hasProcessed(event.eventId)) return { status: 'duplicate' as const };
await repo.reserve(event.data.sku, event.data.quantity);
await repo.markProcessed(event.eventId);
return { status: 'reserved' as const };
}
In production, reserve and markProcessed should share a database transaction or equivalent atomic guarantee. Otherwise, a crash between the two calls can still double-apply the effect. The unit test proves the handler's intended behavior, while a database integration test must prove atomicity:
import { expect, it, vi } from 'vitest';
import { handleOrderCreated } from './handleOrderCreated';
it('does not reserve inventory for an already processed event', async () => {
const repo = {
hasProcessed: vi.fn().mockResolvedValue(true),
reserve: vi.fn().mockResolvedValue(undefined),
markProcessed: vi.fn().mockResolvedValue(undefined)
};
const result = await handleOrderCreated({
eventId: 'evt-dup', eventType: 'order.created', schemaVersion: 1,
occurredAt: '2026-08-03T12:00:00.000Z', correlationId: 'corr-dup',
data: { orderId: 'ord-1', sku: 'SKU-1', quantity: 1 }
}, repo);
expect(result.status).toBe('duplicate');
expect(repo.reserve).not.toHaveBeenCalled();
expect(repo.markProcessed).not.toHaveBeenCalled();
});
Verify the test passes, then add a happy-path case that expects reserve before markProcessed. The idempotency and retries testing guide covers request and event deduplication in more depth.
Step 4: Run a Real Kafka Integration Test
A broker test should answer transport questions that mocks cannot: Can the client connect, serialize, publish, subscribe, and decode with the actual configuration? Use one container per test file and one unique topic per scenario. This prevents retained messages and consumer offsets from leaking between runs.
import { afterAll, beforeAll, expect, it } from 'vitest';
import { Kafka } from 'kafkajs';
import { KafkaContainer, type StartedKafkaContainer } from '@testcontainers/kafka';
import { randomUUID } from 'node:crypto';
let container: StartedKafkaContainer;
beforeAll(async () => { container = await new KafkaContainer("confluentinc/cp-kafka:7.6.0").start(); }, 120_000);
afterAll(async () => { await container.stop(); });
it('delivers one order event through Kafka', async () => {
const kafka = new Kafka({ clientId: 'inventory-test', brokers: [`${container.getHost()}:${container.getMappedPort(9093)}`] });
const admin = kafka.admin();
const producer = kafka.producer();
const consumer = kafka.consumer({ groupId: `inventory-${randomUUID()}` });
const topic = `orders-${randomUUID()}`;
await admin.connect();
await admin.createTopics({ topics: [{ topic, numPartitions: 1, replicationFactor: 1 }] });
await producer.connect();
await consumer.connect();
await consumer.subscribe({ topic, fromBeginning: true });
const received = new Promise<string>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`No message received from ${topic}`)), 10_000);
void consumer.run({ eachMessage: async ({ message }) => {
clearTimeout(timer);
resolve(message.value?.toString() ?? '');
}});
});
await producer.send({ topic, messages: [{ key: 'ord-55', value: JSON.stringify({ eventId: 'evt-55' }) }] });
await expect(received).resolves.toContain('evt-55');
await consumer.disconnect();
await producer.disconnect();
await admin.disconnect();
}, 30_000);
Run npx vitest run src/kafka.integration.test.ts. The first run may take longer while Docker pulls the image. Expect one test to pass and the container to stop even after assertions finish. For more consumer-specific checks, follow Kafka consumer contract testing step by step.
Step 5: Assert Eventual Consistency Without Fixed Sleeps
A fixed setTimeout(5000) makes a test slow when the result appears in 100 ms and flaky when CI needs 5.1 seconds. Poll the business-facing read model until the expected state appears or a strict deadline expires. Include the last observed value in the error so failures explain themselves.
export async function eventually<T>(
read: () => Promise<T>,
accept: (value: T) => boolean,
options = { timeoutMs: 10_000, intervalMs: 100 }
): Promise<T> {
const deadline = Date.now() + options.timeoutMs;
let lastValue: T | undefined;
while (Date.now() < deadline) {
lastValue = await read();
if (accept(lastValue)) return lastValue;
await new Promise(resolve => setTimeout(resolve, options.intervalMs));
}
throw new Error(`Condition not met. Last value: ${JSON.stringify(lastValue)}`);
}
Use it after publishing an event:
const reservation = await eventually(
() => inventoryApi.getReservation('ord-55'),
value => value.status === 200 && value.body.state === 'RESERVED',
{ timeoutMs: 15_000, intervalMs: 200 }
);
expect(reservation.body.quantity).toBe(2);
Verify this helper with fake readers that succeed on the third call and never succeed. Keep the deadline tied to the service-level expectation, not an arbitrary large value. Record correlation ID, topic, partition, offset, consumer group, and last read-model response on timeout. Those details distinguish a lost publish from lag or a projection failure.
Step 6: Exercise Retries, Poison Events, and the Dead-Letter Queue
Classify failures before testing retries. A transient database timeout may merit bounded retries with backoff. An invalid schema or unsupported version is permanent and should not consume the same message repeatedly. Your test must control the failure sequence and assert the final routing decision.
import { expect, it, vi } from 'vitest';
async function consumeWithRetry(
handle: () => Promise<void>,
deadLetter: (reason: string) => Promise<void>,
maxAttempts = 3
) {
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try { await handle(); return; } catch (error) { lastError = error; }
}
await deadLetter(lastError instanceof Error ? lastError.message : String(lastError));
}
it('dead-letters after exactly three failed attempts', async () => {
const handle = vi.fn().mockRejectedValue(new Error('inventory unavailable'));
const deadLetter = vi.fn().mockResolvedValue(undefined);
await consumeWithRetry(handle, deadLetter, 3);
expect(handle).toHaveBeenCalledTimes(3);
expect(deadLetter).toHaveBeenCalledWith('inventory unavailable');
});
Verify one passing test. Add separate cases for success on attempt two, immediate rejection of an unsupported schema version, and a malformed payload that preserves the original bytes plus failure metadata in the dead-letter record. Never assert only that a DLQ message exists. Assert its original event ID, source topic, error category, attempt count, and correlation ID. The dedicated dead-letter queue retry tutorial shows a fuller failure matrix.
Step 7: Add One End-to-End Business Journey
Run an end-to-end test only after the lower layers pass. Create an order through the public API, capture its correlation ID, wait for the inventory read model, and verify the externally visible result. Do not peek directly into every service database because that couples the test to implementation details.
import { expect, it } from 'vitest';
import { eventually } from './eventually';
it('reserves inventory after an order is accepted', async () => {
const correlationId = `e2e-${crypto.randomUUID()}`;
const created = await fetch(`${process.env.ORDER_URL}/orders`, {
method: 'POST', headers: { 'content-type': 'application/json', 'x-correlation-id': correlationId },
body: JSON.stringify({ sku: 'SKU-7', quantity: 2 })
});
expect(created.status).toBe(202);
const { orderId } = await created.json() as { orderId: string };
const response = await eventually(
() => fetch(`${process.env.INVENTORY_URL}/reservations/${orderId}`),
value => value.status === 200,
{ timeoutMs: 20_000, intervalMs: 250 }
);
expect((await response.json() as { quantity: number }).quantity).toBe(2);
});
Verify the test against an isolated environment with known inventory. The suite should also clean up through supported APIs or use unique order IDs and expiring fixtures. One representative happy path plus one important failure path usually gives more value than duplicating every component scenario across a slow deployed stack.
How to Test Event Driven Microservices: Comparison of Test Doubles and Real Brokers
Mocks, embedded substitutes, and containers answer different questions. Choose based on the behavior under test, not convenience alone.
| Option | Fidelity | Isolation | Startup cost | Use it for | Do not rely on it for |
|---|---|---|---|---|---|
| Mock publisher or handler call | Low transport fidelity | Excellent | Near zero | Business rules, mapping, topic choice, headers, error classification | Broker acknowledgments, partitioning, redelivery |
| In-memory fake broker | Medium if behavior is modeled carefully | Excellent | Low | Multi-component orchestration and deterministic failure injection | Exact Kafka or RabbitMQ semantics |
| Testcontainers broker | High | Good with unique resources | Moderate | Client configuration, serialization, routing, offsets, acknowledgments | Production IAM, network policy, managed-service features |
| Shared integration broker | High but stateful | Weak | Already running | Environment configuration and cross-service compatibility | Parallel test determinism |
| Production-like deployed stack | Highest system fidelity | Lowest | High | Critical business journeys and operational readiness | Exhaustive edge cases |
A Kafka container is not automatically superior to a mock. If the question is whether a quantity below one is rejected, a broker merely adds latency. If the question is whether a consumer resumes from a committed offset after restart, a mock provides false confidence. RabbitMQ users can apply the same boundary and use the RabbitMQ event mocking guide for queue-focused examples.
Which Should You Choose
Choose contract tests when teams deploy producers and consumers independently. They catch incompatible field removal, type changes, and enum narrowing before deployment. Add consumer-driven contracts when a producer cannot know which fields each consumer truly depends on.
Choose component tests for the largest scenario matrix. Run valid, invalid, duplicate, stale, out-of-order, and unsupported-version events directly through the handler. Inject repositories, clocks, and publishers so failures are reproducible. These tests should carry most business-rule coverage.
Choose a real broker container for protocol semantics. Cover authentication configuration separately if the local image does not reproduce the managed platform. Test key routing, headers, serialization, consumer group behavior, acknowledgment or offset commits, and redelivery after failure.
Choose a shared environment only for a narrow compatibility suite. Namespace every topic, queue, consumer group, and entity with a run ID. Choose end to end for revenue, safety, or compliance-critical journeys that cross services. A healthy portfolio often resembles a pyramid: many contract and component checks, fewer broker checks, and a handful of deployed journeys. Treat that ratio as guidance, not a quota.
Observability Assertions That Make Failures Diagnosable
An asynchronous assertion needs evidence from each hop. Propagate correlationId unchanged through event headers and structured logs. Record eventId separately because one business trace may contain multiple events. For Kafka, capture topic, partition, offset, consumer group, and lag. For RabbitMQ, capture exchange, routing key, queue, redelivery flag, and dead-letter reason.
Do not make a test pass merely because a log line appeared. Logs are diagnostic evidence, while the assertion should target durable business state or an explicitly promised event. When the state times out, attach recent trace data and the last observed API response to the test report.
Metrics can support nonfunctional checks. Publish a controlled burst, then verify that lag returns to its baseline within a defined test deadline. Force a downstream outage and confirm retry counts grow without an unbounded publish loop. Restore the dependency and verify recovery without manual offset changes. Avoid hard-coded throughput claims unless the environment has controlled resources and a documented service objective.
Schema Evolution and Ordering Scenarios
Backward-compatible evolution is a behavior, not just a schema-registry setting. Replay an older version into the current consumer and prove the business result remains correct. Add an optional field and show that an older consumer ignores it. Reject an unknown required semantic explicitly instead of silently creating corrupt state.
Ordering guarantees usually apply only within a partition or queue, so define the aggregate key. Publish order.created, order.cancelled, and a delayed duplicate for the same order ID. Verify the final state and the version check that prevented regression. Then publish unrelated orders concurrently to ensure the consumer does not impose unnecessary global serialization.
Replay tests deserve permanent fixtures. Store sanitized representative events for every supported schema version, but validate those fixtures against the canonical schemas in modern CI. A fixture that drifts away from the producer is worse than no fixture because it creates believable but obsolete coverage.
Troubleshooting
The consumer receives nothing -> Subscribe before publishing, use a unique consumer group, confirm topic creation, and print broker metadata. In Kafka, check fromBeginning, partition assignment, and whether the producer flushed successfully.
The test passes locally but times out in CI -> Remove fixed sleeps, increase only the bounded deadline, and inspect container CPU plus startup logs. Reuse a container within one file, not mutable topics across parallel files.
Old events contaminate the result -> Generate a unique topic or queue per test run. A new consumer group alone can still read retained history when configured from the beginning.
A duplicate changes inventory twice -> Put the processed-event record and business mutation in one transaction. A separate check followed by a separate write has a race under concurrent delivery.
The DLQ assertion is flaky -> Poll the DLQ by original event ID and correlation ID rather than consuming the first available record. Give each test its own source and dead-letter destination where practical.
The container API differs from the example -> Pin compatible package versions in the lockfile and consult the installed Testcontainers module types. Do not copy methods from a Java example into the Node.js library.
Common Mistakes
- Sleeping for a fixed number of seconds instead of polling a promised outcome with a deadline.
- Reusing topics, queue names, or consumer groups across parallel tests.
- Testing only the happy path and ignoring duplicates, reordering, retries, poison messages, and schema versions.
- Mocking the broker everywhere, which leaves serialization and routing untested.
- Running every case end to end, which creates a slow suite with unclear failures.
- Marking a message processed outside the transaction that changes business state.
- Asserting implementation details in several databases rather than a supported API or emitted event.
- Treating successful publication as proof that a consumer produced the correct business outcome.
- Dropping correlation metadata during republishing or dead-letter routing.
- Sharing production credentials or connecting automated tests to production topics.
Interview Questions and Answers
Interviewers commonly ask you to explain the test pyramid, eventual consistency, idempotency, and contract evolution for asynchronous systems. Strong answers connect each risk to a specific test boundary and observable assertion. The structured interview questions below can be used for rehearsal. For role-specific depth, review microservices contract testing interview questions.
Where To Go Next
Turn this tutorial into a team strategy by inventorying every produced and consumed event. Assign an owner, schema, compatibility policy, partition or routing key, retry rule, and dead-letter destination to each one. Then map at least one automated check to every declared behavior.
Deepen the broker layer with Kafka consumer contract tests, validate failure recovery through dead-letter queue retry testing, and align service boundaries with the event-driven API testing guide. If you are preparing for an SDET role, use the hands-on scenarios in QA practice and compare your evidence against the job requirements in the resume dashboard.
Conclusion: How to Test Event Driven Microservices in CI
The reliable way to test event-driven microservices is to separate fast behavioral evidence from transport and system evidence. Validate contracts on every change, drive producer and consumer logic directly, use a real broker only where its semantics matter, and retain a few observable end-to-end journeys.
Start with one important event. Give it a strict schema, deterministic component tests, a unique-topic broker test, and a bounded business-state assertion. Once duplicates, retries, ordering, and dead-letter behavior are explicit, asynchronous failures become testable engineering cases instead of mysterious timeouts.
Interview Questions and Answers
What testing strategy would you use for event-driven microservices?
I would put most coverage in schema, producer, and consumer component tests. I would add focused broker integration tests for serialization, routing, acknowledgment, offsets, and redelivery, then keep a few deployed end-to-end business journeys. This gives fast defect localization without ignoring infrastructure semantics.
How do you prevent flaky assertions in an eventually consistent system?
I poll an observable business state with a bounded deadline and short interval instead of sleeping for a fixed duration. Each test uses unique correlation data and isolated broker resources. On timeout, I report the last state plus topic, partition, offset, and trace details.
How would you prove a consumer is idempotent?
I send the identical event ID repeatedly and concurrently, then verify the durable business mutation occurred once. I also test the database transaction that combines the mutation with recording the processed event. A handler-only mock test is useful but cannot prove race safety.
What is the difference between contract and broker integration testing?
Contract testing proves that producer output and consumer expectations agree on structure and semantics. Broker integration testing proves transport behavior such as connectivity, serialization, routing, partition keys, acknowledgments, and offsets. Passing one does not imply that the other will pass.
How do you test retries without slowing the suite?
I inject the retry policy or clock at the component boundary and simulate a controlled sequence of transient failures. I assert the exact attempt count, success or dead-letter decision, and metadata. A smaller integration test then verifies that the deployed broker configuration matches the tested policy.
How would you test backward-compatible event evolution?
I validate the new schema against the compatibility policy and replay fixtures from every supported version through the current consumer. I also run the changed producer against consumer expectations. The assertions target business outcomes, not schema validation alone.
What information should be preserved when an event reaches a DLQ?
I preserve the original payload, event ID, event type, schema version, source topic or queue, routing data, and correlation ID. I add the error category, message, attempt count, and failure timestamp. That is enough to diagnose, safely replay, and audit the failure.
When would you use a real broker instead of a mock?
I use a real broker when the risk depends on broker semantics: partition assignment, ordering scope, offset commits, acknowledgment, redelivery, routing keys, or serializer configuration. For pure validation and domain rules, a direct handler or mocked publisher is faster and clearer.
Frequently Asked Questions
How do you test event-driven microservices?
Test them in layers: event schema, producer component, consumer component, real-broker integration, and a small end-to-end suite. Use unique correlation IDs and bounded polling to verify eventual business state without fixed sleeps.
Should Kafka be mocked in integration tests?
Mock Kafka when testing business mapping and error decisions, but use a real containerized broker for serialization, partitioning, offsets, and consumer group behavior. A mock and a broker test provide complementary evidence.
How do you test eventual consistency?
Poll a supported read API until the expected state appears or a defined deadline expires. Report the correlation ID, last observed value, and broker metadata on timeout so the failure is diagnosable.
How do you test duplicate event delivery?
Deliver the same event ID twice and assert that the durable business effect occurs once. Also test concurrent duplicate delivery against the real database transaction, because a sequential unit test cannot expose every race.
What should a dead-letter queue test verify?
Verify retry exhaustion, final routing, and the contents of the dead-letter record. It should preserve the original event identity and payload while adding the failure category, attempt count, source, and correlation metadata.
How can event-driven tests run safely in parallel?
Generate unique topic or queue names, consumer groups, event IDs, and business entities for each run. Avoid shared offsets and retained messages, and clean up through supported APIs or automatic resource expiration.