Kafka for Test Automation — fullstop to blind test assertions.
A practical guide to event-driven BDD automated tests in gauge using Apache Kafka Streams, in-memory state stores, and sample order management scenarios
By Author · 12 min read · Sample data used throughout
1 — The Real Problem: Kafka is Invisible to Your Test Suite
Modern microservices communicate through events. An API call comes in, the service does its work, and the result surfaces as a message on a Kafka topic — not in the HTTP response. This is correct, scalable design. But it creates a testing headache: your test fires a request, gets a 200 OK, and then has absolutely no idea when the downstream event will arrive.
And also in our organization we struggeled to automate API tests when different services which communicate through kafka asyncronuously, couldnt be accurately automate just using API hits only. We wanted to get data from kafka topics to proceed.
The naive fix is Thread.sleep().let kafka do its thing Until our timeout .It looks harmless. It isn’t.
Eight seconds. For every test. If your suite has 200 order-flow tests, you have just committed to 26 minutes of pure waiting — and that’s assuming 8 seconds is always enough. On a busy CI server under load, it often isn’t.
Thread.sleep() doesn’t test your system. It tests your ability to guess how fast your system is on any given day.
The failures are the worst part. When a sleep-based test fails, you get an assertion error that tells you nothing about what actually happened. Did the event never arrive? Did it arrive malformed? Did the service crash? You don’t know. You re-run the test. It passes. You move on, with false confidence and a growing sense of unease.
There is a better way. Instead of guessing when events arrive, we can make the test suite itself a Kafka consumer — a first-class participant that listens to the exact same topics the system writes to, materializes the latest events into memory, and lets BDD steps query them the instant they land.
2 — Solution: Making the Test Suite a Kafka Listener
The core idea is simple: before any API call is made, we spin up a minimal Kafka Streams application inside the test process. This topology subscribes to the topics our system writes to, and continuously materializes the latest event for each key into a local in-memory dynamic array(a state store). When a BDD step wants to verify that an order was created, it queries that local store — not the database, not a retry loop against the API, but the event stream itself.
Topic to Store Mapping
For a sample e-commerce order management system, the mapping looks like this:
Each topic feeds exactly one store. Each store is keyed by the natural business identifier (orderId, productId ) so a query is a simple key lookup. The reduce() operation on the KTable means we always have the latest event, never a stale one.
The Streams Topology Code
3 — Implementation: 5 Steps & we good to go
1.Hook into the test lifecycle
Create a BeforeSuite / @Before hook (or a Gauge/Cucumber execution hook) that starts the Kafka Streams topology before the spec runs. Stop it down using After hooks (No manual wiring needed per test),Make sure the streams are in listening and active before hit APIs.
Get Chamod Jayaweera’s stories in your inbox
Join Medium for free to get updates from this writer.
2.Configure with a unique application ID
Set APPLICATION_ID_CONFIG to a value unique (e.g. Automation _test_listener) or you can use unique id with UUID to remove partition conflicts when multiple CI pipelines run against the same Kafka cluster simultaneously.
NOTE : While a unique ID prevents conflicts, it creates a new Consumer Group and a set of Changelog Topics for every single test run. Over time, this can clutter your Kafka cluster with thousands of orphaned topics and groups. use .withLoggingDisabled(); for that.
3.Set offset reset to ‘latest’ and wait for RUNNING
Start reading from the newest offset — we don’t need history, only events our test triggers. Before firing any API call, poll KafkaStreams.state() until it returns RUNNING. This ensures zero missed events.
4.Build a getEventWithRetry() query helper
Poll the in-memory store at 50ms intervals. Return a typed DTO the moment the event appears, or throw a clear TimeoutException after the configured max wait (e.g. 20 seconds). Never a raw sleep.
5.Write BDD steps that read like assertions, not polling code
Each step calls the helper and asserts on typed fields. The step has no awareness of timing. It either gets the event or it gets a descriptive error.
The Local Store Query Code
4 — Sample BDD Spec: Full Order Flow
Here is a complete Gauge spec (using sample data) that tests the end-to-end order creation and payment flow. Every step after the API call is a real event assertion — zero sleeps.
order-flow.spec
The Step Implementations
5 — Hard-Won Lessons from the Field
!Unique application IDs are non-negotiable
If two test runs share the same APPLICATION_ID_CONFIG against one Kafka cluster, they share a consumer group. Kafka will rebalance partitions between them, dropping events at random. Append a UUID or CI build number to every test run’s app ID.
!Always wait for RUNNING state before triggering APIs
The topology takes a moment to initialize and begin consuming. Fire your first API call before RUNNING and you will miss events published in that window. Poll KafkaStreams.state() in a loop with a 50ms sleep until the state is RUNNING.
!Close every iterator from store.all()
If you scan a full store with store.all() and forget to close the iterator (try-with-resources), the JVM can hang indefinitely during test teardown. Always use: try (KeyValueIterator<K,V> iter = store.all()) { … }
!Set AUTO_OFFSET_RESET to ‘latest’, not ‘earliest’
You want events that your test triggers, not everything since the topic was created. ‘latest’ means: start from now. Combined with the unique app ID (so there’s no committed offset to resume from), you get exactly the events your test produces.
!Write failure messages that tell you what to look for
“Expected order.created event with key ORD-2024–9871 in store.orders. Not found after 20,000ms. Check OrderService logs for this orderId.” is infinitely better than a NullPointerException stack trace.
6 — Results
The most meaningful change wasn’t the speed. It was the easiness of debugging. When a sleep-based test failed, we had no idea whether the bug was in the service, the test, or the timing. Now, a failure means exactly one of two things: the event never arrived (service problem) or the event arrived with wrong data (logic problem). Debugging time dropped from hours to minutes.
The core insight: Thread.sleep() makes your tests a passive observer — always guessing, never knowing. Kafka Streams turns your test suite into an active participant in the same event ecosystem your services live in. The result is tests that are faster, more honest, and far easier to debug. They fail loudly when something is actually wrong, and pass quickly when it isn’t.
All code samples and test data in this article are illustrative. Topic names, order IDs, and business logic are fictional and intended solely to demonstrate the pattern.
Apache Kafka and Kafka Streams are trademarks of The Apache Software Foundation.





