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.
- Receiving Urgent Market Message Push Notifications from Nord Pool - November 1, 2018
- Zen and the Art of Computer Programming - September 2, 2017
- Reading JSON Files to Create Test Versions of REST Clients - April 8, 2017