Hot questions for Using RabbitMQ in junit

Question:

I have a Spring Boot application, and I am trying to send and receive messages via RabbitMQ.

Problem

I can send the messages successfully to the queue (i.e. I see them on the queue in the RabbitMQ Manager), however my Receiver does not receive the messages.

I have a RESTful endpoint I call from JUnit that in turn calls the Sender. While this JUnit test is running the Spring context is loaded as expected, and the Sender is invoked that adds the messages to the queue successfully.

Question

Is there something more I need to do in order to get the Receiver to register so that it will listen for messages? (I suspect that because I am just running the JUnit test, it finishes before the Receiver can listen for messages). Is there a way to keep the test up an running so that the Receiver can consume the messages before it ends?

Code

Sender

@Service
public class RabbitMQSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    @Value("${rabbitmq.exchangename}")
    private String exchange;

    @Value("${rabbitmq.routingkeyname}")
    private String routingkey;  

    public void send(String uuid) {
        rabbitTemplate.convertAndSend(exchange, routingkey, uuid);
        System.out.println("Send RabbitMQ ("+exchange+" "+routingkey+")  msg = " + uuid);       
    }
}

Receiver

public class RabbitMQReceiver {

    @RabbitListener(queues = "${rabbitmq.queuename}")
    public void receive(String in) {
        System.out.println("Received RabbitMQ  msg = " + in);       
    }
}

Configuration

@Configuration
public class RabbitMQConfig {

    @Value("${rabbitmq.queuename}")
    String queueName;

    @Value("${rabbitmq.exchangename}")
    String exchange;

    @Value("${rabbitmq.routingkeyname}")
    String routingkey;

    @Bean
    Queue queue() {
        return new Queue(queueName, false);
    }

    @Bean
    DirectExchange exchange() {
        return new DirectExchange(exchange);
    }

    @Bean
    Binding binding(Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(routingkey);
    }

    @Profile("receiver")
    @Bean
    public RabbitMQReceiver receiver() {
        return new RabbitMQReceiver();
    }

    @Profile("sender")
    @Bean
    public RabbitMQSender sender() {
        return new RabbitMQSender();
    }
}

Answer:

Your sender and receiver doesn't belong to the same profile ! You should includes both profiles in your Junit tests using @ActiveProfiles

@ActivesProfiles({"sender", "receiver"})

Question:

I am integration testing a component that uses RabbitMQ client (amqp-client version 5.7.0) with an embedded Apache Qpid server object.

When calling the Channel.queuePurge() method, the queue is purged. I can verify that with the getMessageCount() method. But the queuePurge() method returns a PurgeOk object, which always has 0 message count. The documentation says the PurgeOK returns the message count that was purged.

   //The queue has 1 message.
   int f = getMessageCount();
   //verify message count
   assertEquals(f, 1);
   //purge
   com.rabbitmq.client.AMQP.Queue.PurgeOk purgeOK= channel.queuePurge(queueName);
   //the next test fails.
   //Shouldn't the purgeOK have count 1, to denote that one message was purged?
   assertEquals(purgeOK.getMessageCount(), 1);

This happens only when testing with embedded Qpid server. The same test case with an actual running instance of Rabbit MQ is giving the expected.

Is this a known issue? Is there a better way to unit test the purge feature?


Answer:

It wasn't a known issue, I have raised an issue for this defect in Qpid Broker-J. Thank you for your help in discovering this.

Question:

I am trying to test the RabbitTemplate#convertAndSend method that is written as a lambda, like so:

// other stuff omitted for brevity

        rabbitTemplate.convertAndSend(myQueue, jsonString, message -> {
        message.getMessageProperties().setPriority(priority);
        return message;
        });

// other stuff omitted for brevity

The test case I am trying to do is one where an ArgumentCaptor is being used in order to verify that the method is called with correct parameters.

@Test
public void givenMyNotification_whenElementIsSent_thenSetPriorityAndSendValidParameters() {

final ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
final int expectedPriority = 5;
final Notification expected = TestUtils.getNotification();

testClass.handleNotification(expected);

verify(rabbitTemplate).convertAndSend(captor.capture(), captor.capture(),
    ArgumentMatchers.eq(MessagePostProcessor.class));

// assertThat...
));

}

The test fails at the verify step because the arguments are different.

Wanted:
<Capturing argument>,
<Capturing argument>,
interface org.springframework.amqp.core.MessagePostProcessor

Actual invocation:
"myQueue",
"myJson",
com.example.notification.service.NotificationService$$Lambda$5/73698537@5bda80bf

I have tried several other Matchers from mockito and hamcrest but to no avail.

So, my questions are:

  1. How does one test this kind of a thing?

  2. Is this even a good practice or are there other/better ways to test rabbit template sending?


Answer:

You almost get it all right, except for the last matcher ArgumentMatchers.eq(MessagePostProcessor.class).

You actually ask Mockito to match for equatility to the class of the parameter. You should rather match against:

  • the type of the parameter using ArgumentMatchers.any(MessagePostProcessor.class)
  • or an actual value ArgumentMatchers.eq(expectedMessageProcessor) if you happen to have it
  • or an argument captor if you do need to check the value of the parameter

In this particular case however, if you use the first option, you may run into a compiler issue, as RabbitTemplate class has two similar methods:

  • convertAndSend(String, String, Object)
  • convertAndSend(String, Object, MessagePostProcessor)

To solve this you can force the type of the second parameter to object like this :

Mockito.verify(rabbitTemplate).convertAndSend(captor.capture(), (Object) captor.capture(),
            Mockito.any(MessagePostProcessor.class));

Or better, have two different ArgumentCaptors for two different parameters:

ArgumentCaptor<String> routingKeyCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Object> messageCaptor = ArgumentCaptor.forClass(Object.class);

...

verify(rabbitTemplate).convertAndSend(routingKeyCaptor.capture(), messageCaptor.capture(), any(MessagePostProcessor.class));

Hope this helps !

Question:

I have a service that is responsible for placing a message on a RabbitMQ queue. I have set up the AMQP config in such a way that I can autowire the AmqpTemplate class into the service. This config works when I move the logic into the body of a JUnit test.

However, When I create a test with the service autowired in and call the method to trigger the AmqpTemplates convertAndSend method nothing happens. Using wireshark I have seen that it still handshakes with the RabbitMQ server but no exchange is created and no messages appear in any queue even when I am making use of RabbitMQ's firehose trace options.

The code is as follows:

<!-- AMQP messaging configurations starts here -->
<!-- Spring AMQP connection factory -->
<rabbit:connection-factory id="connectionFactory"
                           host="localhost"
                           port="5672"
                           username="guest"
                           password="guest"
                           channel-cache-size="25"/>

<!-- Queues -->
<rabbit:queue name="test.queue"/>

<!-- Exchanges with their queue bindings -->
<rabbit:topic-exchange name="test.exchange">
    <rabbit:bindings>
        <rabbit:binding queue="test.queue" pattern="test.*"/>
    </rabbit:bindings>
</rabbit:topic-exchange>

<!-- Spring AMQP template - Creates a bean which can send a message to the topicExchange-->
<rabbit:template id="testTemplate"
                 connection-factory="connectionFactory"
                 exchange="test.exchange"/>

<!-- Spring AMQP Admin -->
<rabbit:admin connection-factory="connectionFactory"/>

The above code segment appears in my application context that is used for the JUnit tests.

@Service
public class AsyncQueueServiceImplementation implements AsyncQueueService
{

  @Autowired
  private AmqpTemplate template;

  @Override
  @Async
  public void publish()
  {
    template.convertAndSend("test.debug", "test payload");
  }
}

The above code segment is the service that is responsible for actually sending an object to the AmqpTemplate. Please not that the AmqpTemplate is autowired in here.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:application-context-unitTests.xml" })
public class AsyncTraceServiceImplementationTest
{

  @Autowired
  AsyncTraceService traceService;

  @Test
  public void testPublishAtDebugLevel()
  {
    traceService.publish();
  }
} 

The above segment is the JUnit test. It uses the application context that contains all the rabbit mq configuration. It then autowires in the service and calls the message.

When I place simple System.out.println's around, I can see that in both the service the AmqpTemplate is instantiated but doesn't seem to do what is expected.

Could this perhaps be an issue with the context not being passed on to the service for some reason.

I have tried using ReflectionTestUtils to set the template field in the service from the Junit test however I was unable to do so.


Answer:

I managed to fix this issue.

The issue was in the RabbitMQ topics that I was using. The exchange received an unknown topic and as a result did nothing with it.

Correcting this resulted in the messages appearing in the correct queue as the exchange was then able to route the messages correctly.