Writing an Integration Test First for RabbitMQ

In a previous post, we saw some Java code for redelivering messages from a queue to an exchange in RabbitMQ. Obviously, a test was written before writing the actual code.

What kind of test is appropriate in this situation? What we want to test is that messages that are in a RabbitMQ queue are removed from that queue and instead available in the queues that are bound to the exchange we move the messages to.

In this case, the important thing is how RabbitMQ behaves as a result of executing our code, so an integration test that connects to a test instance of RabbitMQ running on the developer machine is the right solution. I strongly believe that every developer should have access to a personal instance of the systems that they need to integrate with, as far as possible. This means that the developers should have their own databases, message queues, web containers, and so on, that they can use for testing without disturbing or being disturbed by anyone else. The easiest way to achieve this, in my experience, is to install the systems on each developer machine.

The test declares two exchanges, foo.domain and foo.domain.dlx, and three queues, foo.domain.queue1, foo.domain.queue2 and foo.comain.dlq. The queue foo.domain.queue1 is bound to exchange foo.domain with routing key rk1, and foo.domain.queue2 is bound to the same exchange with routing key rk2. The exchange foo.domain.dlx is set a dead letter exchange for both queues. We then put three messages, foo, bar and baz in the dead letter queue with different routing keys:

public class MoveMessagesIT {

    private static final String EXCHANGE = "foo.domain";
    private static final String EXCHANGE_DLX = "foo.domain.dlx";
    private static final String QUEUE1 = "foo.domain.queue1";
    private static final String QUEUE2 = "foo.domain.queue2";
    private static final String QUEUE_DLX = "foo.domain.dlq";
    private static final String[] TEST_MESSAGES = { "foo", "bar", "baz" };
    private static final String ROUTING_KEY1 = "rk1";
    private static final String ROUTING_KEY2 = "rk2";
    private static final String[] ROUTING_KEYS = { ROUTING_KEY1, ROUTING_KEY2, ROUTING_KEY1 };

    @Before
    public void init() throws IOException {
        Connection connection = connectionFactory().newConnection();
        Channel channel = connection.createChannel();
        // Cleanup from previous test
        channel.queueDelete(QUEUE_DLX);
        channel.queueDelete(QUEUE2);
        channel.queueDelete(QUEUE1);
        channel.exchangeDelete(EXCHANGE_DLX);
        channel.exchangeDelete(EXCHANGE);
        // EXCHANGE/QUEUEs
        channel.exchangeDeclare(EXCHANGE, "topic");
        Map<String, Object> queueArgs = new HashMap<>();
        queueArgs.put("x-message-ttl", 10 * 1000);
        queueArgs.put("x-dead-letter-exchange", EXCHANGE_DLX);
        channel.queueDeclare(QUEUE1, true, false, false, queueArgs);
        channel.queueBind(QUEUE1, EXCHANGE, ROUTING_KEY1);
        channel.queueDeclare(QUEUE2, true, false, false, queueArgs);
        channel.queueBind(QUEUE2, EXCHANGE, ROUTING_KEY2);
        // DLX/DLQ
        channel.exchangeDeclare(EXCHANGE_DLX, "topic");
        channel.queueDeclare(QUEUE_DLX, true, false, false, null);
        channel.queueBind(QUEUE_DLX, EXCHANGE_DLX, "#");
        // Send test messages to DLQ
        for (int i = 0; i < TEST_MESSAGES.length; i++) {
            channel.basicPublish(EXCHANGE_DLX, ROUTING_KEYS[i], null, TEST_MESSAGES[i].getBytes());
        }
        channel.close();
        connection.close();
    }

We also want to verify that the contents of the three queues after moving the messages are as expected, so we create a helper method, verifyMessages that reads messages from a queue, verifying that the message content and routing key are correct:

    private static void verifyMessages(String queue, String routingKey, String... messages) throws IOException {
        Connection connection = connectionFactory().newConnection();
        Channel channel = connection.createChannel();

        List<String> messagesRead = new ArrayList<>();
        while (true) {
            GetResponse response = channel.basicGet(queue, true);
            if (response == null) {
                break;
            }
            Envelope envelope = response.getEnvelope();
            assertThat(envelope.getRoutingKey(), is(routingKey));
            messagesRead.add(new String(response.getBody()));
        }
        channel.close();
        connection.close();
        assertThat(messagesRead, is(Arrays.asList(messages)));
    }

    private static ConnectionFactory connectionFactory() {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        return factory;

    }

We are now ready to add the test method, which is very simple: move all messages from foo.domain.dlq to exchange foo.domain and then verify that the contents of the queues are as expected:

    @Test
    public void moveAllMessagesToExchange() throws Exception {
        MoveMessages moveMessages = new MoveMessages("localhost", "guest", "guest", "/");
        moveMessages.moveAllMessagesToExchange(QUEUE_DLX, EXCHANGE);
        verifyMessages(QUEUE1, ROUTING_KEY1, "foo", "baz");
        verifyMessages(QUEUE2, ROUTING_KEY2, "bar");
        verifyMessages(QUEUE_DLX, null);
    }

Conclusion

We have seen an example of how to write an integration test for RabbitMQ. A few things to note:

  • In this case, an integration test is exactly what is needed since we want to verify how an external system, RabbitMQ, behaves as an effect of running our code. No unit testing is necessary, and mocking the behavior of RabbitMQ in this case would only verify that our mock setup behaves the way that we believe RabbitMQ to behave.
  • Having easy access to your own instance of the systems you integrate with, for example by having them locally installed, makes integration testing much simpler.
  • You often learn useful things about the system you integrate with while you write the integration tests. In this case, the mechanics of getting a connection and channel, and for getting and publishing messages, were already in place before writing any production code.
  • Sometimes the test code is larger and more complex than the resulting code under test. This is OK, but remember to write the test code as cleanly as possible and refactor when necessary.
RealLifeDeveloper

Published by

RealLifeDeveloper

I'm a software developer with 20+ years of experience who likes to work in agile teams using Specification by Example, Domain-Driven Design, Continuous Delivery and lots of automation.

Leave a Reply

Your email address will not be published. Required fields are marked *