web-dev-qa-db-fra.com

Automatisez les tests consommateurs RabbitMQ

J'ai un micro-service .net recevant des messages à l'aide du client RabbitMQ, je dois tester les éléments suivants:

1- Le consommateur est correctement connecté à l'hôte rabbitMq.

2- le consommateur écoute la file d'attente.

3- Le consommateur reçoit des messages avec succès.

Pour atteindre ce qui précède, j'ai créé un exemple d'application qui envoie des messages et je débogue le consommateur pour être sûr qu'il reçoit des messages.

Comment automatiser ce test? par conséquent, l'inclure dans mon CI micro-service.

Je pense inclure mon exemple d'application dans mon CI afin que je puisse déclencher un message puis exécuter un test unitaire consommateur qui attend une heure spécifique puis passe si le message est reçu, mais cela me semble une mauvaise pratique car le test ne démarre pas jusqu'à quelques secondes, le message est déclenché.

Une autre façon de penser est de lancer l'exemple d'application à partir du test unitaire lui-même, mais si l'exemple d'application ne fonctionne pas, cela en ferait un défaut de service.

Existe-t-il des meilleures pratiques pour les tests d'intégration des micro-services se connectant via RabbitMQ?

11
Yahya Hussein

J'ai construit de nombreux tests de ce type. J'ai lancé du code de base sur Github ici avec .NET Core 2.0.

Vous aurez besoin d'un cluster RabbitMQ pour ces tests automatisés. Chaque test commence par éliminer la file d'attente pour s'assurer qu'aucun message n'existe déjà. Les messages préexistants d'un autre test interrompent le test en cours.

J'ai une aide simple pour supprimer la file d'attente. Dans mes applications, ils déclarent toujours leurs propres files d'attente, mais si ce n'est pas votre cas, vous devrez recréer la file d'attente et toutes les liaisons à tous les échanges.

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();
    }
}

J'ai créé un exemple de consommateur très simple qui représente votre microservice. Il s'exécute dans une tâche jusqu'à son annulation.

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();
    }

}

Le consommateur dispose d'une interface IMessageProcessor qui fera le travail de traitement du message. Dans mon test d'intégration, j'ai créé un faux. Vous utiliseriez probablement votre framework de simulation préféré pour cela.

L'éditeur de test publie un message dans la file d'attente.

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);
        }
    }
}

Mon exemple de test ressemble à ceci:

[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]);
}

Mon faux est le suivant:

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

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

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

Des conseils supplémentaires sont:

  • Si vous pouvez ajouter du texte aléatoire à votre file d'attente et échanger des noms à chaque test, faites-le pour éviter que des tests simultanés n'interfèrent les uns avec les autres.

  • J'ai également des aides dans le code pour déclarer les files d'attente, les échanges et les liaisons, si vos applications ne le font pas.

  • Écrivez une classe de suppression de connexion qui forcera les connexions étroites et vérifiera que vos applications fonctionnent toujours et peuvent récupérer. J'ai du code pour cela, mais pas dans .NET Core. Il suffit de me le demander et je peux le modifier pour qu'il s'exécute dans .NET Core.

  • En général, je pense que vous devriez éviter d'inclure d'autres microservices dans vos tests d'intégration. Si vous envoyez un message d'un service à un autre et attendez un message par exemple, créez un faux consommateur qui peut se moquer du comportement attendu. Si vous recevez des messages d'autres services, créez de faux éditeurs dans votre projet de test d'intégration.

8
Vanlightly

J'ai réussi ce genre de test. Vous avez besoin d'une instance de test de RabbitMQ, d'un test d'échange pour envoyer des messages et d'une file d'attente de test pour vous connecter afin de recevoir des messages.

Ne vous moquez pas de tout!

Mais, avec le consommateur de test, le producteur et l'instance de test de rabbitMQ, il n'y a pas de code de production réel dans ce test.

utiliser l'instance test rabbitMQ et une véritable application

Afin d'avoir un test significatif, j'utiliserais l'instance de test RabbitMQ, l'échange et la file d'attente, mais laisser une application réelle (producteur et consommateur).

Je mettrais en œuvre le scénario suivant

  1. lorsque l'application de test fait quelque chose qui teste le message à rabbitMQ

  2. alors le nombre de messages reçus dans rabbitMQ est augmenté puis

  3. l'application fait quelque chose qu'elle devrait faire lors de la réception des messages

Les étapes 1 et 3 sont spécifiques à l'application. Votre application envoie des messages à rabbitMQ en fonction d'un événement externe (message HTTP reçu? Événement du minuteur?). Vous pouvez reproduire une telle condition dans votre test, donc l'application enverra un message (pour tester l'instance rabbitMQ).

Même histoire pour vérifier l'action de l'application lors de la réception du message. L'application doit faire quelque chose d'observable lors de la réception des messages. Si l'application effectue un appel HTTP, vous pouvez vous moquer de ce point de terminaison HTTP et vérifier les messages reçus. Si l'application enregistre des messages dans la base de données, vous pouvez regrouper la base de données pour rechercher votre message.

utilisez l'API de surveillance rabbitMQ

L'étape 2 peut être implémentée à l'aide de l'API de surveillance RabbitMQ (il existe des méthodes pour voir le nombre de messages reçus et consommés de la file d'attente https://www.rabbitmq.com/monitoring.html#rabbitmq-metrics )

envisagez d'utiliser Spring Boot pour effectuer des contrôles de santé

Si vous êtes basé sur Java et que l'utilisation de Spring Boot simplifie considérablement votre problème. Vous obtiendrez automatiquement un bilan de santé pour votre connexion rabbitMQ!

Voir https://spring.io/guides/gs/messaging-rabbitmq/ pour un tutoriel sur la façon de se connecter à RabbitMQ à l'aide de Spring Boot. L'application Spring Boot expose les informations d'intégrité (en utilisant le point de terminaison/l'intégrité HTTP) pour chaque ressource externe attachée (base de données, messagerie, jms, etc.) Voir https://docs.spring.io/spring-boot/docs/current/ reference/html/production-ready-endpoints.html # _auto_configured_healthindicators pour plus de détails.

Si la connexion à rabbitMQ est interrompue, le contrôle de santé (effectué par org.springframework.boot.actuate.amqp.RabbitHealthIndicator) renverra le code HTTP 4xx et un message json signifiant dans le corps JSON.

Vous n'avez rien à faire de particulier pour avoir ce bilan de santé - il suffit d'utiliser org.springframework.boot: spring-boot-starter-amqp car la dépendance maven/gradle suffit.

CI test- depuis le répertoire src/test

J'ai écrit un tel test (qui se connecte à une instance de test externe de RabbitMQ) à l'aide de tests d'intégration, dans le répertoire src/test. Si vous utilisez Spring Boot, il est plus facile de le faire en utilisant le profil de test et en ayant les détails de la connexion pour tester l'instance RabbitMQ dans application-test.properties (la production peut utiliser le profil de production et le fichier application-production.properties avec l'instance de production de RabbitMQ).

Dans le cas le plus simple (vérifiez simplement la connexion à rabbitMQ), tout ce dont vous avez besoin est de démarrer l'application normalement et de valider le point de terminaison/health.

Dans ce cas, je ferais les étapes CI suivantes

  • celui qui construit (gradle build)
  • celui qui exécute des tests unitaires (tests sans dépendances externes)
  • celui qui exécute des tests d'intégration

Test CI - externe

L'approche décrite ci-dessus pourrait également être effectuée pour l'application déployée dans l'environnement de test (et connectée à l'instance de test rabbitMQ). Dès que l'application démarre, vous pouvez vérifier le point de terminaison/health pour vous assurer qu'il est connecté à l'instance rabbitMQ.

Si vous demandez à votre application d'envoyer un message à rabbitMQ, vous pouvez observer les métriques rabbbitMQ (en utilisant l'API de surveillance rabbitMQ) et observer les effets externes du message consommé par l'application.

Pour un tel test, vous devez démarrer et déployer votre application à partir de CI avant de démarrer les tests.

pour ce scénario, je ferais en suivant les étapes CI

  • étape qui construit l'application
  • étapes qui exécutent tous les tests dans le répertoire src/test (unité, intégration)
  • étape qui déploie l'application pour tester l'environnement ou démarre l'application ancrée
  • étape qui exécute des tests externes
  • pour un environnement docké, étape qui arrête les conteneurs dockers

Envisagez un environnement docké

Pour un test externe, vous pouvez exécuter votre application avec l'instance de test RabbitMQ dans Docker. Vous aurez besoin de deux conteneurs docker.

Pour exécuter ces deux images, il est plus raisonnable d'écrire un fichier de composition de docker.

5
Bartosz Bilicki