Search code examples
apache-kafkaspring-cloudapache-kafka-streamsspring-kafkaspring-cloud-stream

How can I test a Spring Cloud Stream Kafka Streams application that uses Avro and the Confluent Schema Registry?


I am having trouble figuring out how to test a Spring Cloud Stream Kafka Streams application that uses Avro as message format and a (Confluent) schema registry.

The configuration could be something like this:

spring:
  application:
    name: shipping-service
  cloud:
    stream:
      schema-registry-client:
        endpoint: http://localhost:8081
      kafka:
        streams:
          binder:
            configuration:
              application:
                id: shipping-service
              default:
                key:
                  serde: org.apache.kafka.common.serialization.Serdes$IntegerSerde
              schema:
                registry:
                  url: ${spring.cloud.stream.schema-registry-client.endpoint}
              value:
                subject:
                  name:
                    strategy: io.confluent.kafka.serializers.subject.RecordNameStrategy
          bindings:
            input:
              consumer:
                valueSerde: io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde
            order:
              consumer:
                valueSerde: io.confluent.kafka.streams.serdes.avro.GenericAvroSerde
            output:
              producer:
                valueSerde: io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde
      bindings:
        input:
          destination: customer
        order:
          destination: order
        output:
          destination: order

server:
  port: 8086

logging:
  level:
    org.springframework.kafka.config: debug

NOTES:

  • It is using native serialization/deserialization.
  • Test framework: Junit 5

I guess regarding the Kafka Broker I should use a EmbeddedKafkaBroker bean, but as you see, it also relies on a Schema Registry that should be mocked in some way. How?


Solution

  • Sorting this out has been a real pain, but finally I managed to make it work using fluent-kafka-streams-tests:

    Extra dependencies:

    testImplementation("org.springframework.kafka:spring-kafka-test")
    testImplementation("com.bakdata.fluent-kafka-streams-tests:schema-registry-mock-junit5:2.0.0")
    

    The key is to set up the necessary configs as System properties. For that I created a separated test configuration class:

    @Configuration
    class KafkaTestConfiguration(private val embeddedKafkaBroker: EmbeddedKafkaBroker) {
    
        private val schemaRegistryMock = SchemaRegistryMock()
    
        @PostConstruct
        fun init() {
            System.setProperty("spring.kafka.bootstrap-servers", embeddedKafkaBroker.brokersAsString)
            System.setProperty("spring.cloud.stream.kafka.streams.binder.brokers", embeddedKafkaBroker.brokersAsString)
            schemaRegistryMock.start()
            System.setProperty("spring.cloud.stream.schema-registry-client.endpoint", schemaRegistryMock.url)
            System.setProperty("spring.cloud.stream.kafka.streams.binder.configuration.schema.registry.url", schemaRegistryMock.url)
        }
    
        @Bean
        fun schemaRegistryMock(): SchemaRegistryMock {
            return schemaRegistryMock
        }
    
        @PreDestroy
        fun preDestroy() {
            schemaRegistryMock.stop()
        }
    }
    

    Finally the test class, where you can now produce and consume Avro messages with your KStream processing them and taking advantage of the mock schema registry:

    @EmbeddedKafka
    @SpringBootTest(properties = [
        "spring.profiles.active=local",
        "schema-registry.user=",
        "schema-registry.password=",
        "spring.cloud.stream.bindings.event.destination=event",
        "spring.cloud.stream.bindings.event.producer.useNativeEncoding=true",
        "spring.cloud.stream.kafka.streams.binder.configuration.application.server=localhost:8080",
        "spring.cloud.stream.kafka.streams.bindings.event.consumer.keySerde=io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde",
        "spring.cloud.stream.kafka.streams.bindings.event.consumer.valueSerde=io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde"])
    class MyApplicationTests {
    
        @Autowired
        private lateinit var embeddedKafka: EmbeddedKafkaBroker
    
        @Autowired
        private lateinit var schemaRegistryMock: SchemaRegistryMock
    
        @Test
        fun `should process events`() {
            val senderProps = KafkaTestUtils.producerProps(embeddedKafka)
            senderProps[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = "io.confluent.kafka.serializers.KafkaAvroSerializer"
            senderProps[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = "io.confluent.kafka.serializers.KafkaAvroSerializer"
            senderProps["schema.registry.url"] = schemaRegistryMock.url
            val pf = DefaultKafkaProducerFactory<Int, String>(senderProps)
            try {
                val template = KafkaTemplate(pf, true)
                template.defaultTopic = "event"
                ...
    
        }