Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automate RabbitMQ consumer testing

I have a .net micro-service receiving messages using RabbitMQ client, I need to test the following:

1- consumer is successfully connected to rabbitMq host.

2- consumer is listening to queue.

3- consumer is receiving messages successfully.

To achieve the above, I have created a sample application that sends messages and I am debugging consumer to be sure that it is receiving messages.

How can I automate this test? hence include it in my micro-service CI.

I am thinking to include my sample app in my CI so I can fire a message then run a consumer unit test that waits a specific time then passes if the message received, but this seems like a wrong practice to me because the test will not start until a few seconds the message is fired.

Another way I am thinking of is firing the sample application from the unit test itself, but if the sample app fails to work that would make it the service fault.

Is there any best practices for integration testing of micro-services connecting through RabbitMQ?

like image 414
Yahya Hussein Avatar asked May 04 '18 14:05

Yahya Hussein


People also ask

How to create a consumer application using RabbitMQ queue?

Now let's create a consumer application which will receive message from the RabbitMQ Queue. Using eclipse, select File → New → Maven Project. Tick the Create a simple project (skip archetype selection) and click Next.

Is it possible to test RabbitMQ with production code?

But, with test consumer, producer and test instance of rabbitMQ there is no actual production code in that test. In order to have meaniningfull test I would use test RabbitMQ instance, exchange and queue, but leave real application (producer and consumer).

When are consumers recovered in RabbitMQ?

In other words, consumers are usually recovered last, after their target queues and those queues' bindings are in place. Applications can subscribe to have RabbitMQ push enqueued messages (deliveries) to them. This is done by registering a consumer (subscription) on a queue. After a subscription is in place, RabbitMQ will begin delivering messages.

Do not mock everything in RabbitMQ tests?

Do not mock everything! But, with test consumer, producer and test instance of rabbitMQ there is no actual production code in that test. In order to have meaniningfull test I would use test RabbitMQ instance, exchange and queue, but leave real application (producer and consumer).


2 Answers

I was successfully doing such kind of test. You need test instance of RabbitMQ, test exchange to send messages to and test queue to connect to receive messages.

Do not mock everything!

But, with test consumer, producer and test instance of rabbitMQ there is no actual production code in that test.

use test rabbitMQ instance and real aplication

In order to have meaniningfull test I would use test RabbitMQ instance, exchange and queue, but leave real application (producer and consumer).

I would implement following scenario

  1. when test application does something that test message to rabbitMQ

  2. then number of received messages in rabbitMQ is increased then

  3. application does something that it should do upon receiving messages

Steps 1 and 3 are application-specific. Your application sends messages to rabbitMQ based on some external event (HTTP message received? timer event?). You could reproduce such condition in your test, so application will send message (to test rabbitMQ instance).

Same story for verifying application action upon receiving message. Application should do something observable upon receiving messages. If application makes HTTP call- then you can mock that HTTP endpoint and verify received messages. If application saves messages to the database- you could pool database to look for your message.

use rabbitMQ monitoring API

Step 2 can be implemented using RabbitMQ monitoring API (there are methods to see number of messages received and consumed from queue https://www.rabbitmq.com/monitoring.html#rabbitmq-metrics)

consider using spring boot to have health checks

If you are java-based and then using Spring Boot will significantly simpify your problem. You will automatically get health check for your rabbitMQ connection!

See https://spring.io/guides/gs/messaging-rabbitmq/ for tutorial how to connect to RabbitMQ using Spring boot. Spring boot application exposes health information (using HTTP endpoint /health) for every attached external resource (database, messaging, jms, etc) See https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html#_auto_configured_healthindicators for details.

If connection to rabbitMQ is down then health check (done by org.springframework.boot.actuate.amqp.RabbitHealthIndicator) will return HTTP code 4xx and meaninfull json message in JSON body.

You do not have to do anything particular to have that health check- just using org.springframework.boot:spring-boot-starter-amqp as maven/gradle dependency is enough.

CI test- from src/test directory

I have written such test (that connect to external test instance of RabbitMQ) using integration tests, in src/test directory. If using Spring Boot it is easiest to do that using test profile, and having details of connection to test RabbitMQ instance in application-test.properties (production could use production profile, and application-production.properties file with production instance of RabbitMQ).

In simplest case (just verify connection to rabbitMQ) all you need is to start application normally and validate /health endpoint.

In this case I would do following CI steps

  • one that builds (gradle build)
  • one that run unit tests (tests without any external dependenices)
  • one that run integration tests

CI test- external

Above described approach could also be done for application deployed to test environment (and connected to test rabbitMQ instance). As soon as application starts, you can check /health endpoint to make sure it is connected to rabbitMQ instance.

If you make your application send message to rabbitMQ, then you could observe rabbbitMQ metrics (using rabbitMQ monitoring API) and observe external effects of message being consumed by application.

For such test you need to start and deploy your application from CI befor starting tests.

for that scenario I would do following CI steps

  • step that that builds app
  • steps that run all tests in src/test directory (unit, integration)
  • step that deploys app to test environment, or starts dockerized application
  • step that runs external tests
  • for dockerized environment, step that stops docker containers

Consider dockerized enevironment

For external test you could run your application along with test RabbitMQ instance in Docker. You will need two docker containers.

  • one with application
  • one with rabbitMQ . There is official docker image for rabbitmq https://hub.docker.com/_/rabbitmq/ and it is really easy to use

To run those two images, it is most reasonable to write docker-compose file.

like image 67
Bartosz Bilicki Avatar answered Sep 26 '22 13:09

Bartosz Bilicki


I have built many such tests. I have thrown up some basic code on GitHub here with .NET Core 2.0.

You will need a RabbitMQ cluster for these automated tests. Each test starts by eliminating the queue to ensure that no messages already exist. Pre existing messages from another test will break the current test.

I have a simple helper to delete the queue. In my applications, they always declare their own queues, but if that is not your case then you'll have to create the queue again and any bindings to any exchanges.

public class QueueDestroyer
{
    public static void DeleteQueue(string queueName, string virtualHost)
    {
        var connectionFactory = new ConnectionFactory();
        connectionFactory.HostName = "localhost";
        connectionFactory.UserName = "guest";
        connectionFactory.Password = "guest";
        connectionFactory.VirtualHost = virtualHost;
        var connection = connectionFactory.CreateConnection();
        var channel = connection.CreateModel();
        channel.QueueDelete(queueName);
        connection.Close();
    }
}

I have created a very simple consumer example that represents your microservice. It runs in a Task until cancellation.

public class Consumer
{
    private IMessageProcessor _messageProcessor;
    private Task _consumerTask;

    public Consumer(IMessageProcessor messageProcessor)
    {
        _messageProcessor = messageProcessor;
    }

    public void Consume(CancellationToken token, string queueName)
    {
        _consumerTask = Task.Run(() =>
        {
            var factory = new ConnectionFactory() { HostName = "localhost" };
            using (var connection = factory.CreateConnection())
            {
                using (var channel = connection.CreateModel())
                {
                    channel.QueueDeclare(queue: queueName,
                                    durable: false,
                                    exclusive: false,
                                    autoDelete: false,
                                    arguments: null);

                    var consumer = new EventingBasicConsumer(channel);
                    consumer.Received += (model, ea) =>
                    {
                        var body = ea.Body;
                        var message = Encoding.UTF8.GetString(body);
                        _messageProcessor.ProcessMessage(message);
                    };
                    channel.BasicConsume(queue: queueName,
                                        autoAck: false,
                                            consumer: consumer);

                    while (!token.IsCancellationRequested)
                        Thread.Sleep(1000);
                }
            }
        });
    }

    public void WaitForCompletion()
    {
        _consumerTask.Wait();
    }
}

The consumer has an IMessageProcessor interface that will do the work of processing the message. In my integration test I created a fake. You would probably use your preferred mocking framework for this.

The test publisher publishes a message to the queue.

public class TestPublisher
{
    public void Publish(string queueName, string message)
    {
        var factory = new ConnectionFactory() { HostName = "localhost", UserName="guest", Password="guest" };
        using (var connection = factory.CreateConnection())
        using (var channel = connection.CreateModel())
        {
            var body = Encoding.UTF8.GetBytes(message);

            channel.BasicPublish(exchange: "",
                                    routingKey: queueName,
                                    basicProperties: null,
                                    body: body);
        }
    }
}

My example test looks like this:

[Fact]
public void If_SendMessageToQueue_ThenConsumerReceiv4es()
{
    // ARRANGE
    QueueDestroyer.DeleteQueue("queueX", "/");
    var cts = new CancellationTokenSource();
    var fake = new FakeProcessor();
    var myMicroService = new Consumer(fake);

    // ACT
    myMicroService.Consume(cts.Token, "queueX");

    var producer = new TestPublisher();
    producer.Publish("queueX", "hello");

    Thread.Sleep(1000); // make sure the consumer will have received the message
    cts.Cancel();

    // ASSERT
    Assert.Equal(1, fake.Messages.Count);
    Assert.Equal("hello", fake.Messages[0]);
}

My fake is this:

public class FakeProcessor : IMessageProcessor
{
    public List<string> Messages { get; set; }

    public FakeProcessor()
    {
        Messages = new List<string>();
    }

    public void ProcessMessage(string message)
    {
        Messages.Add(message);
    }
}

Additional advice is:

  • If you can append randomized text to your queue and exchange names on each test run then do so to avoid concurrent tests interfering with each other

  • I have some helpers in the code for declaring queues, exchanges and bindings also, if your applications don't do that.

  • Write a connection killer class that will force close connections and check your applications still work and can recover. I have code for that, but not in .NET Core. Just ask me for it and I can modify it to run in .NET Core.

  • In general, I think you should avoid including other microservices in your integration tests. If you send a message from one service to another and expect a message back for example, then create a fake consumer that can mock the expected behaviour. If you receive messages from other services then create fake publishers in your integration test project.

like image 31
Vanlightly Avatar answered Sep 25 '22 13:09

Vanlightly