0

I have a Kafka consumer which is using an @KafkaListener annotation. To write the test cases using an embedded Kafka server, I followed the code here: How to write Unit test for @KafkaListener?. The suggested approach in the sample code is not working. The only difference in my code is that the value is not a String. So I configured the producerFactory accordingly with the JsonSerializer.

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
@DirtiesContext
public class SpringKafkaReceiverTest {

    private static String RECEIVER_TOPIC="messageRouterTopic";
    
    @ClassRule
    public static EmbeddedKafkaRule embeddedKafka = new EmbeddedKafkaRule(1, true, RECEIVER_TOPIC);
    
    @Mock
    Processor processor;
    
    @Autowired
    private KafkaListenerEndpointRegistry registry;
    
    private KafkaTemplate<String, Envelope> template;
    
    
    @Bean
    private ProducerFactory<String, Envelope> producerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, embeddedKafka.getEmbeddedKafka().getBrokersAsString());
        props.put(ProducerConfig.RETRIES_CONFIG, 0);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory<>(props);
    }
    
    @Before
    public void setup() {
        
        System.setProperty("spring.kafka.bootstrap-servers", embeddedKafka.getEmbeddedKafka().getBrokersAsString());
        template = new KafkaTemplate<>(producerFactory());
        MockitoAnnotations.initMocks(this);
    }
    
    @Test
    public void test() throws Exception {
        ConcurrentMessageListenerContainer<?, ?> container = (ConcurrentMessageListenerContainer<?, ?>) registry.getListenerContainer(RECEIVER_TOPIC);
        
        container.stop();
        
        @SuppressWarnings("unchecked")
        AcknowledgingConsumerAwareMessageListener<String, Envelope> messageListener = (AcknowledgingConsumerAwareMessageListener<String, Envelope>) container
                .getContainerProperties().getMessageListener();
        
        
        CountDownLatch latch = new CountDownLatch(1);
    
        container.getContainerProperties().setMessageListener(new AcknowledgingConsumerAwareMessageListener<String, Envelope>(){

            @Override
            public void onMessage(ConsumerRecord<String, Envelope> data, Acknowledgment acknowledgment,
                    Consumer<?, ?> consumer) {
                messageListener.onMessage(data, acknowledgment, consumer);
                latch.countDown();
            }
            
        });
        
        container.start();
        Envelope message = new Envelope();
        message.setAction("sampleAction");
        message.setValue("This is the object for testing purpose");

        // These are the void methods
        // Since these methods will be invoked when the listener method listens to the Kafka topic, it is not explicitly called here.
         
        doNothing().when(processor).setCommand(any());
        doNothing().when(processor).allocateEnvelopes(any());

      
        template.send(RECEIVER_TOPIC, message);
        verify(processor, Mockito.times(1)).allocateEnvelopes(any());
        //assertTrue(latch.await(10, TimeUnit.SECONDS));
    }
}

The class which bootstraps the application is:

@SpringBootApplication
public class PostOfficeApplication {

    private static ApplicationContext appContext;
    
    public static void main(String[] args) {
        appContext = SpringApplication.run(PostOfficeApplication.class, args);
    }

    
    public static ApplicationContext getApplicationContext() {
        return appContext;
    }
    
    
}

The Kafka listener code is as below:

    @Bean
    public ConsumerFactory<String, Envelope> consumerFactory(List<String> consumerBootstrapServers) {
        Map<String, Object> config = new HashMap<>();
        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, consumerBootstrapServers);
        config.put(ConsumerConfig.GROUP_ID_CONFIG, "postOfficeGrp");
        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
        config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");

        return new DefaultKafkaConsumerFactory<>(config, new StringDeserializer(),
                new JsonDeserializer<>(Envelope.class));
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, Envelope> kafkaListenerFactory(ConsumerFactory<String, Envelope> consumerFactory) {
        ConcurrentKafkaListenerContainerFactory<String, Envelope> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory);
        factory.setErrorHandler(new SeekToCurrentErrorHandler());
        return factory;
    }

@Service
public class Mailbox {

    @Autowired
    Processor processor;
            
    @KafkaListener(id="messageRouterTopic" , topics = "${app.topic}", groupId = "postOfficeGrp", containerFactory = "kafkaListenerFactory")
    public void processKafkaMessages(Envelope object) {

    processor.setCommand(PostOfficeApplication.getApplicationContext().getBean(object.getAction(), Command.class));
    processor.allocateEnvelopes(object);
        
    }
}

And when I run mvn test, I see the error as below in the console:

[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 6.975 s <<< FAILURE! - in com.walmartlabs.gioda.po.consumer.SpringKafkaReceiverTest
[ERROR] test  Time elapsed: 0.246 s  <<< ERROR!
java.lang.NullPointerException
    at SpringKafkaReceiverTest.test(SpringKafkaReceiverTest.java:84)

[INFO] 
[INFO] Results:
[INFO] 
[ERROR] Errors: 
[ERROR]   SpringKafkaReceiverTest.test:84 NullPointer
[INFO] 
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  9.286 s
[INFO] Finished at: 2020-08-07T10:07:09-05:00
[INFO] ------------------------------------------------------------------------

Every time I run the code, container.stop() fails with NullPointerException. How can I solve this? Am I missing anything?

Swapnil
  • 801
  • 3
  • 19
  • 42
  • 1
    You need to show the test code and stack trace. – Gary Russell Aug 07 '20 at 13:25
  • @GaryRussell: I added the code snippet here. The code is almost similar to whatever is mentioned in the referenced question, so I did not put it here. Apologies for that! – Swapnil Aug 07 '20 at 15:14
  • The `NullPointerException` is in your test itself; which is line #84? – Gary Russell Aug 07 '20 at 15:34
  • Yeah, the line #84 in my editor is container.stop(). – Swapnil Aug 07 '20 at 15:41
  • Show your `@KafkaListener`. – Gary Russell Aug 07 '20 at 16:10
  • Added the code for Listener. Let me know if anything else is needed. – Swapnil Aug 07 '20 at 16:39
  • 1
    `registry.getListenerContainer(RECEIVER_TOPIC)` You need to give the listener an `id` and use that value to get the container from the registry, not the topic name. – Gary Russell Aug 07 '20 at 17:21
  • Great! Your suggested approach is working fine. But the listener method has a dependency on another object. Since I am not mocking the class in which the listener method is written, how can I mock the dependency of that method? Due to this, when it invokes the listener method, it can not find the dependent object and throws NullPointerException. I added the code to explain how I am doing that. – Swapnil Aug 07 '20 at 17:47
  • I don't see how the auto wiring can succeed if there is no `Processor` bean - why can't you simply mock that bean - you can then verify the expected methods are called. – Gary Russell Aug 07 '20 at 18:05
  • I added the Mockito verify method. Since the Mailbox object (which has the listener method and the dependency on ```Processor``` object) is not invoked explicitly, I am a little confused about how to use the Mockito verify. Otherwise, I would have used ```InjectMock``` annotation. Can you please guide me? – Swapnil Aug 07 '20 at 20:57
  • >`is not invoked explicitly,` - that doesn't make sense; you can't mock `Mailbox`; you need to have the real `Mailbox` as a `@Bean` so that we find the `@KafkaListener`. You need to mock the `Processor` field and verify on that. – Gary Russell Aug 07 '20 at 21:16
  • I mocked ```Processor``` field. If you don't mind can you edit my code to explain how ```Mailbox``` will resolve the dependency on ```Processor``` bean? – Swapnil Aug 07 '20 at 21:24
  • You need to add the mock `Processor` as a Bean in the test context so Spring will auto wire it in. – Gary Russell Aug 08 '20 at 13:08
  • I tried to add the mock ```Processor```. This time mockito verify method is failing with an error: ```Actually, there are zero interactions with this mock```. Can you please check my updated code? – Swapnil Aug 08 '20 at 18:48

0 Answers0