0

How do you unit test a class that implements the spring-kafka MessageListener interface? I have a listener class that I am manually listening to topics with a onMessage function. This function is very simple and just receives the messages.

My setup is with Spring 5.8, Spring-Kafka 2.2.7, Spring-Kafka-Test, JUnit, and WITHOUT spring boot.

I have been trying a bunch of different examples from the Spring reference docs and other posts but none seem to show a simple way to test the Listener class that implements MessageListener.

I am not sure if I need to set up an EmbeddedKafkaBroker or EmbeddedKafkaRule or is there a different way to test. When I tried using EmbeddedKafkaRule I get an error that says NoClassDefFound.

However I don't understand how this test case would hit my onMessage function.

@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class listenerTest {

    private String topic = "someTopic";

    @ClassRule
    public static EmbeddedKafkaRule embeddedKafka = new EmbeddedKafkaRule(1, true, topic);

    private CountDownLatch countDownLatch;

    @Before
    public void setUpTests (){
        Map<String, Object> sProps = KafkaTestUtils.senderProps(embeddedKafka.getEmbeddedKafka().getBrokersAsString());

        ProducerFactory producer = new DefaultKafkaProducerFactory<String, String> (sProps);

        kafkaTemplate = new KafkaTemplate<>(producer);

        kafkaTemplate.setDefaultTopic(topic);

        countDownLatch = new CountDownLatch (1);

    }

    @Test
    public void testReceiver(){
         kafkaTemplate.sendDefault("message");
         assertEquals(latch.getCount(), 0);
    }

Class I want to Unit Test

public class listener implements BatchAcknowledgingMessageListener<String, String>{

    private CallbackInterface callback;

    public listener(CallbackInterface callback){
        this.callbackI = callback;
    }

    @Override
    public void onMessage(List<ConsumerRecord<String, String>> records, Acknowledgment ack){
         this.callbackI.handleMessage();
         ack.acknowledge();
    }
}

This throws a weird error that says this... NoClassDefFound

Clev_James23
  • 171
  • 1
  • 3
  • 15
  • 1
    you are trying to test framework functionality – Ryuzaki L Jul 25 '19 at 05:52
  • How would I go about testing both the framework functionality and also improve my code coverage for that onMessage class/function? When I tried to directly call the onMessage function to test it I get an error that says Container should not be calling function onMessage. – Clev_James23 Jul 25 '19 at 11:22
  • Need not to be you can `Autowire` this `listener` and call the method by passing `List`, i'm not saying it is wrong but still you can do that framework functionality test also – Ryuzaki L Jul 25 '19 at 11:24
  • I am receiving a class rule must implement test rule error when running this test – Clev_James23 Jul 25 '19 at 11:55
  • can you show that error message? – Ryuzaki L Jul 25 '19 at 12:27
  • ```org.junit.internals.runners.rules.ValidationError: The @ClassRule 'embeddedKafka' must implement TestRule``` – Clev_James23 Jul 25 '19 at 12:43

1 Answers1

2

For a pure unit test, you don't need an embedded broker, you should just call the listener directly.

Inject a mock callback and verify it was called properly.

When I tried to directly call the onMessage function to test it I get an error that says Container should not be calling function onMessage.

You are calling the wrong onMessage...

public interface BatchMessageListener extends MessageListener {

    @Override
    default void onMessage(Message message) {
        throw new UnsupportedOperationException("Should never be called by the container");
    }

    @Override
    void onMessageBatch(List<Message> messages);

}

EDIT

public class MyListener implements BatchAcknowledgingMessageListener<String, String> {

    private final MyService service;

    public MyListener(MyService service) {
        this.service = service;
    }

    @Override
    public void onMessage(List<ConsumerRecord<String, String>> data, Acknowledgment acknowledgment) {
        data.forEach(dat -> this.service.call(dat.value()));
        acknowledgment.acknowledge();
    }

    public interface MyService {

        void call(String toCall);

    }

}

and

class So57192362ApplicationTests {

    @Test
    void test() {
        MyService service = mock(MyService.class);
        MyListener listener = new MyListener(service);
        Acknowledgment acknowledgment = mock(Acknowledgment.class);
        listener.onMessage(Collections.singletonList(new ConsumerRecord<>("foo", 0, 0L, null, "bar")), acknowledgment);
        verify(service).call("bar");
        verify(acknowledgment).acknowledge();
        verifyNoMoreInteractions(service, acknowledgment);
    }

}
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • I am actually implementing BatchAcknowledgingMessageListener... When we use onMessageBatch I see that onMessage is still a required function how does spring know which ones for messages to get consumed on? Does it make sense to have both on the class and just leave onMessage empty and leave my implmentation in onMessageBatch? – Clev_James23 Jul 25 '19 at 17:10
  • Your original question had `BatchMessageListener` (before the edit). The container calls `ListenerUtils.determineListenerType()` to determine which method to call. You only need to implement the required method on the interface you selected, but your test must call that too. – Gary Russell Jul 25 '19 at 17:48
  • Apologies for the mix up. So I checked the Git page for the BatchAcknowledgingMessageListener and it only implements onMessage. So when I call onMessage in the unit test I will still run into the same issue. How would you go about testing in that scenario? – Clev_James23 Jul 25 '19 at 17:53