No rewrites

Keep your production Kafka beans

Dynamic bean overrides are for applications with named Kafka beans that include production-only broker or auth settings. Bootstrap is rewritten in place, security is stripped, serialisers are preserved.

One property turns it on:

Properties
wiretap.test.bean-overrides.enabled=true

With that one property, the override also applies listener-friendly consumer defaults:

  • auto.offset.reset is set to earliest
  • configured consumer group.id values get a unique test suffix for the Spring context

What gets adjusted

When the feature is enabled, the library looks for these Spring Kafka beans:

  • ProducerFactory
  • ConsumerFactory
  • KafkaTemplate
  • KafkaAdmin
  • ConcurrentKafkaListenerContainerFactory
  • DefaultShareConsumerFactory and ShareKafkaListenerContainerFactory (share groups, see share groups)

Those beans keep their original names. Existing @Qualifier values, @KafkaListener(containerFactory = "...") references, and any code that injects a named KafkaTemplate continue to resolve in the same way.

The library then points the Kafka client configuration at the shared broker and removes production security settings such as security.protocol, sasl.*, ssl.*, and nested client auth settings.

producer config
 {
-  "bootstrap.servers": "prod-kafka.example.internal:9093",
-  "security.protocol": "SASL_SSL",
-  "sasl.mechanism": "PLAIN",
-  "sasl.*": "production auth settings",
+  "bootstrap.servers": "localhost:62672",
   "key.serializer": StringSerializer.class,
   "value.serializer": CustomEventSerializer.class
 }

Map-built configuration

The override does not require teams to use the standard spring.kafka.* properties. Applications that build Kafka config directly in Java maps are supported too:

KafkaConfig.java
@Bean("eventConsumerFactory")
public ConsumerFactory<String, CustomEvent> eventConsumerFactory() {
    Map<String, Object> props = new HashMap<>();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, productionBootstrapServers);
    props.put(ConsumerConfig.GROUP_ID_CONFIG, "event-consumer");
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
    props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, CustomEventDeserializer.class);
    return new DefaultKafkaConsumerFactory<>(props);
}

With dynamic overrides enabled, that consumer factory keeps the key deserialiser, ErrorHandlingDeserializer, and delegate deserialiser. The broker, test-incompatible security settings, and listener-friendly consumer defaults are changed.

consumer config
 {
-  "bootstrap.servers": "prod-kafka-map.example.invalid:9093",
-  "security.protocol": "SASL_SSL",
-  "sasl.mechanism": "PLAIN",
+  "bootstrap.servers": "localhost:62672",
   "group.id": "event-consumer-wiretap-9f4c...",
   "auto.offset.reset": "earliest",
   "key.deserializer": StringDeserializer.class,
   "value.deserializer": ErrorHandlingDeserializer.class,
   "spring.deserializer.value.delegate.class": CustomEventDeserializer.class
 }

The same applies to producer factories built with custom serialisers:

KafkaConfig.java
@Bean("customEventKafkaTemplate")
public KafkaTemplate<String, CustomEvent> customEventKafkaTemplate() {
    Map<String, Object> props = new HashMap<>();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, productionBootstrapServers);
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, CustomEventSerializer.class);
    return new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(props));
}

JSON, Avro and Protobuf

JSON configurations are handled the same way as any other map-built client. The post-processor keeps Spring Kafka JSON serialiser settings, including default value type and type-header settings:

JSON consumer
 {
-  "bootstrap.servers": "prod-kafka-map.example.invalid:9093",
-  "security.protocol": "SASL_SSL",
+  "bootstrap.servers": "localhost:62672",
   "value.deserializer": JsonDeserializer.class,
   "spring.json.value.default.type": "com.example.CustomEvent",
   "spring.json.use.type.headers": false
 }

For Avro or Protobuf, enable the mock Schema Registry as usual:

Properties
wiretap.test.mock-schema-registry.enabled=true
wiretap.test.bean-overrides.enabled=true

If an Avro or Protobuf producer or consumer factory was built with a production Schema Registry URL, the override points it at the mock registry and removes common registry auth settings. The post-processor recognises either serialiser by class name — KafkaAvro* or KafkaProtobuf* — so a KafkaProtobufSerializer/Deserializer factory is retargeted exactly like an Avro one:

Avro producer
 {
-  "bootstrap.servers": "prod-kafka-map.example.invalid:9093",
-  "schema.registry.url": "https://schema-registry.example.internal",
-  "basic.auth.credentials.source": "USER_INFO",
-  "schema.registry.basic.auth.user.info": "prod:secret",
+  "bootstrap.servers": "localhost:62672",
+  "schema.registry.url": "mock://wiretap",
   "value.serializer": KafkaAvroSerializer.class
 }

Listener factories

Named listener container factories are common in applications with several event types or acknowledgement modes. Dynamic overrides preserve the listener factory bean and update its consumer factory.

KafkaConfig.java
@Bean("eventKafkaListenerContainerFactory")
public ConcurrentKafkaListenerContainerFactory<String, CustomEvent> eventKafkaListenerContainerFactory(
        ConsumerFactory<String, CustomEvent> eventConsumerFactory) {
    ConcurrentKafkaListenerContainerFactory<String, CustomEvent> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(eventConsumerFactory);
    factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
    return factory;
}

Share consumer factories

A DefaultShareConsumerFactory is retargeted through its bootstrap-servers supplier, because the share factory exposes no updateConfigs. When it carries production security keys, the bean is replaced by a sanitised copy under the same name, with the original deserialisers and factory listeners, so @KafkaListener(containerFactory = "...") references still resolve. A ShareKafkaListenerContainerFactory gets the same group-id suffix as the classic factory, and each suffixed share group receives share.auto.offset.reset from wiretap.test.bean-overrides.share.auto-offset-reset (default earliest) before its containers start, so seeded fixtures reach share listeners too.

Consumer defaults & opt-out

Consumer factories are made safe for listener-driven tests by default:

consumer defaults
 {
   "bootstrap.servers": "localhost:62672",
-  "group.id": "event-consumer",
-  "auto.offset.reset": "latest",
+  "group.id": "event-consumer-wiretap-9f4c...",
+  "auto.offset.reset": "earliest",
   "value.deserializer": CustomEventDeserializer.class
 }

If a test class is deliberately testing committed offsets or shared group behaviour, opt out of one or both defaults:

Properties
wiretap.test.bean-overrides.consumer.auto-offset-reset=preserve
wiretap.test.bean-overrides.consumer.unique-group-ids=false

You can also provide a deterministic suffix if stable group names are useful for debugging:

Properties
wiretap.test.bean-overrides.consumer.group-id-suffix=-my-suite

When to keep manual test beans

Keep a manual @TestConfiguration override when the test needs a different behaviour, not just a different broker:

  • replacing the production serialiser or deserialiser with a test double
  • changing the consumer group strategy for a specific test class
  • removing application-specific listener advice, filters, interceptors, or error handling
  • testing against a deliberately different topic layout

Things to check