When testing a Kafka application, how do you decide what belongs in a unit test versus an integration test, and which tools fit each side?
answer
- real broker needed? -> integration
- Mock*/TopologyTestDriver -> unit
- @EmbeddedKafka / KafkaContainer -> integration
- async => awaitility, no sleep
- test pyramid: many unit, few e2e
basics
~10 sUnit tests check your logic without a real broker, using MockProducer/MockConsumer or TopologyTestDriver. Integration tests run against a real (embedded or containerized) broker to verify serialization, partitioning, and offset behavior end to end.
solid answer
~40 sDraw the boundary at 'does this test need a real broker?'. Unit tests isolate your own logic — message mapping, error handling, business rules — and replace the client with a test double like MockProducer or MockConsumer, or run a Kafka Streams topology with TopologyTestDriver. They are fast, deterministic, and in-process. Integration tests verify things only a real broker exercises: serializers/deserializers, partition assignment, consumer-group rebalancing, offset commits, transactions, and config wiring. For those you spin up @EmbeddedKafka (in-JVM broker) or a Testcontainers KafkaContainer (real broker in Docker). A common pyramid: many unit tests over your handlers/serdes, fewer integration tests over the producer→broker→consumer path, and a handful of full end-to-end tests. Integration tests need awaitility-style polling because delivery is asynchronous.
go deeper
Know the basic split: mocks/TopologyTestDriver = no broker = unit; embedded/Testcontainers = real broker = integration.
Can name the specific tool per side and justify which broker-mediated behaviors force an integration test.
Articulates the test pyramid, fidelity vs speed tradeoffs, and why async forces polling-based assertions.
Sets team policy on the unit/integration boundary, CI cost budgets, and which fidelity tier (embedded vs Testcontainers) each layer uses.
Kafka is a distributed messaging system: producers write records to topics (split into partitions) on a broker, and consumers read them, tracking their position with committed offsets. Testing such an application means deciding how much of that machinery you actually run. **Unit tests** verify your own code in isolation, with no network and no broker. You replace the Kafka client with a test double: - `MockProducer<K,V>` (in `org.apache.kafka.clients.producer`) records every `send()` into an in-memory `history()` list instead of talking to a broker. You assert what your code tried to send. - `MockConsumer<K,V>` lets you pre-load records with `addRecord(...)` and drive `poll()` returns, so you can test consumer-side logic without a broker. - For Kafka Streams, `TopologyTestDriver` runs a whole `Topology` synchronously in-process: you pipe input records in and read output records out with no broker at all. These tests are milliseconds-fast and fully deterministic. **Integration tests** exercise behavior that only a real broker produces: real serialization/deserialization (a wrong serde only fails against a broker), partitioning (which key lands on which partition), consumer-group coordination and rebalancing, offset commit semantics, transactions/exactly-once, and Spring/config wiring. Two standard tools: - `@EmbeddedKafka` (Spring Kafka) starts a broker **inside the test JVM** — fast startup, no Docker, but it is not a production-identical broker. - Testcontainers `KafkaContainer` starts a **real Kafka broker in a Docker container** — production-fidelity (real broker image/version), at the cost of needing Docker and slower startup. Because Kafka delivery is **asynchronous**, integration assertions cannot be 'send then immediately assert'. You poll until a condition holds, typically with **Awaitility** (`await().atMost(...).untilAsserted(...)`), which retries the assertion until it passes or times out — avoiding both flaky fixed `Thread.sleep` and false failures. **The boundary rule**: if the thing you want to verify is your logic, unit-test it with a double or TopologyTestDriver. If it is a broker-mediated behavior (serde, partitioning, offsets, rebalance, transactions), integration-test it. Follow a test pyramid — many fast unit tests, fewer integration tests, minimal full end-to-end — so the suite stays fast and the broker-dependent surface stays small.
- Why can't a serializer bug be caught by a MockProducer unit test?MockProducer keeps records in memory and never invokes the configured serializer against the broker, so a broken or mismatched serde slips through. Only a real broker (embedded or Testcontainers) actually serializes and would surface it.
- Why is Thread.sleep a bad way to wait in an integration test?It either wastes time (sleep too long) or flakes (sleep too short) because async delivery latency varies. Awaitility polls the assertion until it passes within a timeout, so it's both faster on average and far less flaky.
saying these in an interview costs you the question
- Claiming MockProducer/MockConsumer talk to a real broker — they don't, they're in-memory doubles.
- Saying TopologyTestDriver needs an embedded broker — it runs the topology fully in-process with no broker.
- Using Thread.sleep instead of awaitility polling for async assertions.
- Putting everything in integration tests, making the suite slow and flaky.