JUnit Testing
Kafka Consumer
Software Development
Programming
Java

Writing JUnit tests for Kafka Consumer

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Kafka consumers are easiest to test when you separate "poll from Kafka" from "process a record." Once those concerns are split, you can use fast unit tests for business logic and a smaller number of integration tests to verify Kafka wiring.

Test the Processing Logic First

If your consumer method does everything inside an endless poll loop, tests become awkward. A better design is to isolate record handling in a small method:

java
1public class OrderHandler {
2    public String handle(ConsumerRecord<String, String> record) {
3        return "processed:" + record.value();
4    }
5}

That logic is trivial to test with ordinary JUnit:

java
1@Test
2void handlesRecordValue() {
3    OrderHandler handler = new OrderHandler();
4    ConsumerRecord<String, String> record =
5        new ConsumerRecord<>("orders", 0, 10L, "k1", "created");
6
7    String result = handler.handle(record);
8
9    assertEquals("processed:created", result);
10}

These tests run fast and catch most application bugs before Kafka infrastructure is even involved.

Unit Testing Polling with MockConsumer

When you want to test consumer-loop behavior without a broker, Kafka’s MockConsumer is useful. It lets you inject records and offsets programmatically:

java
1@Test
2void pollsSingleRecord() {
3    String topic = "orders";
4    MockConsumer<String, String> consumer =
5        new MockConsumer<>(OffsetResetStrategy.EARLIEST);
6
7    TopicPartition partition = new TopicPartition(topic, 0);
8    consumer.assign(List.of(partition));
9    consumer.updateBeginningOffsets(Map.of(partition, 0L));
10    consumer.addRecord(new ConsumerRecord<>(topic, 0, 0L, "k1", "created"));
11
12    ConsumerRecords<String, String> records = consumer.poll(Duration.ZERO);
13
14    assertEquals(1, records.count());
15    assertEquals("created", records.iterator().next().value());
16}

This is ideal for verifying offset handling, empty polls, and simple processing loops without booting Kafka.

Integration Tests with a Real Broker

Mocking is not enough for serializer issues, group coordination, or actual topic interaction. For that, use an embedded broker or Testcontainers.

A lightweight Testcontainers setup looks like this:

java
1@Testcontainers
2class KafkaConsumerIT {
3
4    @Container
5    static KafkaContainer kafka =
6        new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.1"));
7
8    @Test
9    void consumesProducedMessage() throws Exception {
10        String bootstrap = kafka.getBootstrapServers();
11
12        Properties producerProps = new Properties();
13        producerProps.put("bootstrap.servers", bootstrap);
14        producerProps.put("key.serializer", StringSerializer.class.getName());
15        producerProps.put("value.serializer", StringSerializer.class.getName());
16
17        try (KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps)) {
18            producer.send(new ProducerRecord<>("orders", "k1", "created")).get();
19        }
20
21        Properties consumerProps = new Properties();
22        consumerProps.put("bootstrap.servers", bootstrap);
23        consumerProps.put("group.id", "test-group");
24        consumerProps.put("auto.offset.reset", "earliest");
25        consumerProps.put("key.deserializer", StringDeserializer.class.getName());
26        consumerProps.put("value.deserializer", StringDeserializer.class.getName());
27
28        try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps)) {
29            consumer.subscribe(List.of("orders"));
30            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10));
31            assertFalse(records.isEmpty());
32        }
33    }
34}

Use this level of testing sparingly. It is slower than unit tests, but it validates the real wiring.

What to Assert

Good Kafka consumer tests usually verify:

  • deserialization succeeds
  • records are processed in the expected way
  • offsets are committed or acknowledged at the right time
  • poison messages are handled safely
  • retries or dead-letter behavior trigger when appropriate

That is more valuable than simply asserting that poll() returns a non-empty result.

Common Pitfalls

The biggest mistake is putting all consumer behavior inside an infinite loop with no test seam. That forces every test to become a brittle integration test.

Another common issue is relying only on MockConsumer. It is great for logic, but it does not prove your serializers, broker connectivity, or topic configuration are correct.

Teams also forget test isolation. Reusing the same topic or group ID across tests can create flaky results because previous offsets leak into later runs.

Finally, do not assert too early in asynchronous tests. Give the consumer enough time to join the group and fetch data, especially in broker-backed integration tests.

Summary

  • Split record processing from Kafka polling so most behavior can be unit tested.
  • Use plain JUnit for handler logic and MockConsumer for consumer-loop logic.
  • Use Testcontainers or an embedded broker for serializer and integration coverage.
  • Isolate topics and consumer groups per test to avoid flaky offsets.
  • Assert meaningful outcomes such as processing, retries, and commit behavior.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.