BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Test Des Applications Web Quarkus : Reactive Messaging, Kafka Et Testcontainers

Test Des Applications Web Quarkus : Reactive Messaging, Kafka Et Testcontainers

Points Clés

  • Quarkus est un framework Java natif Kubernetes complet qui prend en charge de nombreux styles de codage, y compris la programmation réactive.
  • La rédaction de tests unitaires / composants / d'intégration propres pour les applications Quarkus lorsqu'une approche réactive est utilisée est d'une importance vitale. Nous présentons ici des tests de code réactif, de messagerie réactive et de test d'intégration complète.
  • Écrire des tests pour valider du code réactif peut sembler compliqué, et la stratégie souvent suivie peut consister à simuler les classes ou à créer un cluster Kafka. Cependant, il existe d'autres alternatives.
  • Il est important d'écrire des tests d'intégration pour valider que tout fonctionne dans des circonstances similaires à celles de la production. Lors de la gestion du compromis entre réalisme et temps d'exécution des tests, vous pouvez utiliser Testcontainers pour initialiser des instances éphémères de bases de données et des journaux / files d'attente distribués.
  • Les tests unitaires sont un pilier central pour maintenir la qualité des applications, mais les tests de composants et d'intégration sont également importants. Quarkus excelle dans la prise en charge de ce type de tests.

Quarkus est un framework Java natif Kubernetes full-stack conçu pour les machines virtuelles Java (JVM) et la compilation native, optimisant Java spécifiquement pour les conteneurs et lui permettant de devenir une plate-forme efficace pour les environnements serverless, cloud et Kubernetes.

Au lieu de réinventer la roue, Quarkus utilise des frameworks d'entreprise bien connus soutenus par des normes / spécifications et les rendent compilables en binaire à l'aide de Graal VM.

Dans cet article, nous allons apprendre à écrire des tests unitaires / composants / d'intégration propres pour les applications Quarkus lorsqu'une messagerie réactive est utilisée. Nous allons voir comment nous pouvons écrire des tests simples et propres pour les scénarios suivants :

  • Code réactif
  • Messagerie réactive
  • Test d'intégration complet

Voyons comment nous pouvons écrire des tests pour ces cas d'utilisation.

L'application

Prenons un petit cas d'utilisation d'un processus de paiement pour une boutique en ligne qui vend des livres.

Le processus de paiement se compose des étapes suivantes :

  1. Le panier est reçu par le service de paiement.
  2. Un appel HTTP synchrone est effectué vers le service de remise pour appliquer une éventuelle remise.
  3. L'achat est stocké dans la base de données.
  4. Un événement est émis vers un topic Kafka.
  5. L'événement est reçu de manière asynchrone par le service de livraison.
  6. Le service de livraison traite l'achat (c'est-à-dire prépare le colis, l'envoie au client, etc.).

Nous n'allons pas inspecter le code de l'application en profondeur, mais uniquement les parties nécessaires aux tests.

Le service d'encaissement

Le service d'encaissement est un endpoint de l'API REST implémentant le processus de paiement du panier.

@Inject
CheckoutProcess checkoutProcess;

@POST
@Transactional
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response checkout(ShoppingBasket shoppingBasket) {

    Long id = checkoutProcess.checkout(shoppingBasket);

    UriBuilder uriBuilder = UriBuilder.fromResource(ShopResource.class);
    uriBuilder.path(Long.toString(id));

    return Response.created(uriBuilder.build()).build();
}

La logique métier est déléguée à la classe CheckoutProcess.

La classe CheckoutProcess

Cette classe implémente le flux vers le processus de paiement. Les étapes impliquées dans le processus sont :

  1. l'appel du service de remise est effectué pour appliquer toute remise sur le prix final.
  2. les données sont conservées dans la base de données.
  3. un événement est émis vers le système de messagerie.
import javax.json.bind.JsonbBuilder;

import org.eclipse.microprofile.reactive.messaging.Emitter;
import org.eclipse.microprofile.reactive.messaging.Channel;

@ApplicationScoped
public class CheckoutProcess {

   @Inject
   PriceService priceService;

   @Inject
   @Channel("delivery")
   Emitter<String> delivery;

   public Long checkout(ShoppingBasket shoppingBasket) {
       Double total = this.priceService.calculate(shoppingBasket);
      
       shoppingBasket.persist();
      
       Invoice invoice = new Invoice();
       invoice.shoppingBasket = shoppingBasket;
       invoice.total = total;

       invoice.persist();

       delivery.send(JsonbBuilder.create().toJson(shoppingBasket));

       return invoice.id;

   }
}

●    La classe Emitter envoie un message d'événement au chanel configuré
●    L'annotation Channel définit delivery comme chanel où les messages sont envoyés
●    L'objet Java du panier d'achat est marshalé en String à l'aide de la spécification JSON-B et envoyé en tant que corps de l'événement

La classe DeliveryService

Cette classe écoute tout événement émis dans le système de messagerie et le traite.

import org.eclipse.microprofile.reactive.messaging.Incoming;

@ApplicationScoped
public class DeliveryService {
 
   @Incoming("delivery-warehouse")
   public void deliverPurchase(String shoppingBasket) {
       final ShoppingBasket cart = JsonbBuilder.create().fromJson(shoppingBasket, ShoppingBasket.class);

       System.out.println(String.format("Sending to %s the following items %s", cart.address.toString(), cart.toString()));

   }

}

●    L'annotation Incoming configure le chanel où les événements sont lus
●    La méthode deliveryPurchase est appelée pour chaque événement et le contenu du corps est injecté en tant que paramètre

La configuration

Enfin, nous devons configurer la connexion à la base de données, le cluster Kafka et le système de messagerie à l'aide du fichier application.properties.

quarkus.datasource.db-kind=mariadb
quarkus.datasource.username=developer
quarkus.datasource.password=developer
quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/mydb
quarkus.hibernate-orm.database.generation = drop-and-create

%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.username=developer
%test.quarkus.datasource.password=developer
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:mydb
%test.quarkus.hibernate-orm.database.generation = drop-and-create

org.acme.DiscountGateway/mp-rest/url=http://localhost:9000

%prod.kafka.bootstrap.servers=localhost:9092
%prod.mp.messaging.outgoing.delivery.connector=smallrye-kafka
%prod.mp.messaging.incoming.delivery-warehouse.connector=smallrye-kafka

mp.messaging.incoming.delivery-warehouse.topic=delivery
mp.messaging.incoming.delivery-warehouse.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
mp.messaging.outgoing.delivery.topic=delivery
mp.messaging.outgoing.delivery.value.serializer=org.apache.kafka.common.serialization.StringSerializer

La première chose que vous remarquerez peut-être ici est que certaines propriétés commencent par un %. Pas de soucis, nous allons les expliquer dans la section suivante car c'est un concept important qui simplifie la configuration des tests dans Quarkus.

  • Dans la première partie du fichier, deux sources de données sont définies - une par default pour se connecter à un serveur MariaDB et une seconde utilisée pendant la phase de test.
  • Ensuite, l'emplacement du service Discount à l'aide de l'extension MicroProfile Rest client .
  • Enfin, la configuration de Kafka et des systèmes de messagerie utilisée uniquement en phase de production (JAR/packaging natif).

Concernant la configuration du système de messagerie, chacun des chanels utilisés dans l'application (delivery et delivery-warehouse) doit être configuré, sinon, un chanel en mémoire sera utilisé par défaut et c'est un aspect clé pour tester ces types d'applications comme nous allons le voir dans un instant.

Mais avant de passer au sujet de tests, voyons ce que représente exactement ce % au début de certaines lignes et pourquoi il est vraiment utile pour les tests.

Les profils Quarkus

Quarkus prend en charge la notion de profils de configuration. Ceux-ci vous permettent d'avoir plusieurs valeurs de configuration dans le même fichier et de choisir entre elles via un nom de profil.

La syntaxe pour cela est .config.key = valeur .

Quarkus est livré avec des profils par défaut :

  • dev : activé en mode développement (quarkus:dev)
  • test : activé lors de l'exécution des tests
  • prod : le profil par défaut lorsqu'il n'est pas exécuté en mode développement ou test

Les propriétés non précédées d'un profil sont utilisées comme valeurs par défaut si elles ne sont spécifiées dans aucun autre profil.

Donc, si nous regardons de nouveau le fichier de configuration utilisé précédemment, nous remarquerons que :

  • Une base de données H2 en mémoire est utilisée lors de l'exécution des tests, dans tous les autres cas, MariaDB est utilisée comme base de données.
  • La messagerie réactive avec Kafka n'est utilisée que lorsque le profil de production est activé. Dans tous les autres cas, aucune configuration n'est fournie.

Vous pouvez créer des noms de profils personnalisés et les activer en définissant la propriété système quarkus.profile ou la variable d'environnement QUARKUS_PROFILE.

quarkus.http.port=9090
%staging.quarkus.http.port=9999

Et activez le profil staging en démarrant Quarkus avec l'option
-Dquarkus.profile=staging.

Les fichiers supplémentaires

Parfois, nous avons plusieurs propriétés de configuration à définir, et le fait de les avoir toutes ensemble aboutit rapidement à un gros fichier de configuration rendant les modifications de n'importe quelle propriété sujettes à erreur. Pour éviter ce problème, il existe la propriété smallrye.config.locations utilisée pour définir des fichiers de configuration supplémentaires.

Le principal avantage de cette approche est que nous pouvons avoir un seul fichier de configuration par environnement d'exécution :
application.properties, application-staging.properties, application-it.properties.

Par exemple, si les propriétés de staging sont requises, nous démarrons l'application Quarkus en définissant la propriété système suivante : -Dsmallrye.config.locations=application-staging.properties.

La définition de profils dans les tests

Par défaut, lors de l'exécution des tests, les propriétés de configuration du profil de test sont chargées et utilisées pendant l'exécution du test. Mais certains tests peuvent nécessiter des valeurs de configuration de test différentes. Quarkus vous donne cette flexibilité en utilisant l'annotation TestProfile. Les profils de test doivent implémenter l'interface QuarkusTestProfile pour définir les nouveaux paramètres du test.

Toutes les méthodes de l'interface QuarkusTestProfile sont par défaut, il n'est donc pas obligatoire de toutes les implémenter, nous devons seulement implémenter celles qui sont nécessaires selon la nature du test.

import io.quarkus.test.junit.QuarkusTestProfile;

public class MyTestProfile implements QuarkusTestProfile {
 
   @Override
   public Map<String, String> getConfigOverrides() {
       return Collections.singletonMap("greeting.message"," HW");
   }

   @Override
   public Set<Class<?>> getEnabledAlternatives() {
       return Collections.emptySet();
   }


   @Override
   public String getConfigProfile() {
       return "staging";
   }

   @Override
   public List<TestResourceEntry> testResources() {
       return Collections.emptyList();
   }

   @Override
   public boolean disableGlobalTestResources() {
       return false;
   }

   @Override
   public Set<String> tags() {
       return Collections.singleton("test1");
   }

}
  • getConfigOverrides renvoie une config supplémentaire à appliquer au test. Cela remplace toute configuration existante.
  • getEnabledAlternatives définit une liste d'alternatives CDI à utiliser dans le test.
  • getConfigProfile active le profil à utiliser dans le test. Il peut être test (c'est celui par défaut) ou tout autre profil personnalisé.
  • testResources définit une liste de QuarkusTestResourceLifecycleManager.
  • disableGlobalTestResources renvoie true si seules les ressources de test renvoyées par testResources sont démarrées, et les ressources de test annotées globales sont ignorées.
  • tags définir les noms de balises auxquels ce profil est associé.
@QuarkusTest
@TestProfile(MyTestProfile.class)
public class MyQuarkusTest {}

Le profil MyTestProfile est appliqué lorsque MyQuarkusTest est exécuté.

Le balisage (Tagging)

Un profil de test peut être catégorisé à l'aide de balises. Dans l'exemple précédent, MyTestProfile est catégorisé avec la balise test1. Pour limiter l'exécution des tests par leurs noms des balises, la propriété quarkus.test.profile.tags peut être définie avec les balises de test à exécuter. Si elle n'est pas définie, tous les tests sont exécutés.

Dans la section suivante, nous allons voir un exemple réel de profils Quarkus.

Maintenant que nous savons comment utiliser les profils Quarkus et comment ils peuvent nous aider à des fins de test, allons de l'avant et apprenons à écrire des tests de composants pour la partie de messagerie réactive.

Tests de composants pour la messagerie réactive

À ce stade, nous voulons écrire un test de composant pour tes traitements d'encaissement afin de valider que toutes les parties qui composent le processus fonctionnent et que les intégrations avec le framework Quarkus fonctionnent comme prévu. Bien sûr, comme il s'agit d'un test de composant, nous ne voulons pas avoir de dépendances externes (c'est-à-dire une base de données distante, un cluster Kafka) car cela pourrait ralentir notre suite de tests de composants ou même la rendre bancale.

Le CheckoutProcess a 3 dépendances externes que nous devons traiter (service de prix, la persistance et le cluster Kafka):

1.    @Inject PriceService priceService;
2.    shoppingBasket.persist();
3.    @Inject @Channel("delivery") Emitter<String> delivery;

Le service de prix (PriceService)

Il existe plusieurs stratégies pour tester le service de prix ; nous pouvons choisir d'utiliser des mocks, des stubs, ou un outillage de virtualisation de services comme Hoverfly pour tester la pile de communication complète. Nous avons couvert cette approche dans la deuxième partie de cette série d'articles sur les tests Tester les applications Web Quarkus : Écrire De Tests De Composants Propres.

La persistance

Ce sujet a été abordé dans la première partie de cette série d'articles sur les tests Tester les applications Web de Quarkus : Tests de composants & d'intégration. Pour ce cas précis, et comme vous avez pu le remarquer dans le fichier application.properties, une base de données en mémoire est utilisée.

Le cluster Kafka

L'écriture de tests pour valider du code réactif peut sembler compliquée, et la stratégie que vous pourriez suivre pourrait être soit de mocker les classes, soit de spin up un cluster Kafka. Mais cette tentation est loin de la réalité grâce au fonctionnement de la messagerie réactive.

Quarkus utilise MicroProfile Reactive Messaging pour interagir avec Apache Kafka. Par défaut, lorsqu'un stream (ou channel) n'est pas configuré pour se connecter à un broker de messages comme Apache Kafka ou Artemis, alors il est considéré comme un stream en mémoire rendant la communication entre les flux rapide et fiable.

Pour des test, un flux en mémoire est la meilleure approche car aucun serveur externe n'est nécessaire mais nous devons également valider le contenu envoyé sur le flux. Ceci n'est pas supporté par défaut mais le projet MicroProfile Reactive Messaging fournit un artefact pour obtenir le contenu du channel en mémoire.

Enregistrons la dépendance smallrye-reactive-messaging-in-memory afin d'avoir accès au flux in-memory depuis nos tests.

Par exemple dans Maven, vous devez ajouter la section suivante dans le pom.xml :

<dependency>
  <groupId>io.smallrye.reactive</groupId>
  <artifactId>smallrye-reactive-messaging-in-memory</artifactId>
  <scope>test</scope>
</dependency>

Alors, le connecteur doit être configuré pour être celui en mémoire au lieu de celui de Kafka. Dans ce cas, le profil de test est utilisé pour configurer les connecteurs en mémoire uniquement à des fins de test.

%test.mp.messaging.outgoing.delivery.connector=smallrye-in-memory
%test.mp.messaging.incoming.delivery-warehouse.connector=smallrye-in-memory

Test de la classe CheckoutProcess

La classe CheckoutProcessTest n'est pas très différent des autres tests Quarkus, la seule grande différence est que la classe InMemoryConnector est injectée pour inspecter le contenu en streaming.

import io.smallrye.reactive.messaging.connectors.InMemoryConnector;
import io.smallrye.reactive.messaging.connectors.InMemorySink;

@QuarkusTest
@Transactional
public class CheckoutProcessTest {

   @Inject
   CheckoutProcess checkoutProcess;

   @Inject @Any
   InMemoryConnector connector;

   @Test
   public void should_apply_checkout() {
       
       ShoppingBasket shoppingBasket = ShoppingBasketObjectMother.theHobbiBasket();
       checkoutProcess.checkout(shoppingBasket);

       Invoice invoce = Invoice.findInvoiceByTransaction(ShoppingBasketObjectMother.TRANSACTION_ID);

       assertThat(invoce.id).isNotNull();

       InMemorySink<String> queue = connector.sink("delivery");
       String payload = queue.received().get(0).getPayload();

       final ShoppingBasket cart = JsonbBuilder.create().fromJson(payload, ShoppingBasket.class);
       assertThat(cart.transactionId).isEqualTo(ShoppingBasketObjectMother.TRANSACTION_ID);

   }
}
  • La méthode checkout stocke les paniers dans la base de données, génère un identifiant de transaction et émet un événement vers le channel delivery.
  • Les messages envoyés au channel delivery sont stockés dans un sink en mémoire nommé delivery.
  • Utiliser l'instance InMemorySink pour récupérer les messages et faire des asserts dessus dans le test.

Test de la classe DeliveryService Test

La classe DeliveryServiceTest vérifie qu'un événement est traité correctement. Si c'est le cas, le InMemoryConnector est utilisé pour émettre un événement vers le stream delivery-warehouse.

import io.smallrye.reactive.messaging.connectors.InMemoryConnector;
import io.smallrye.reactive.messaging.connectors.InMemorySource;

@QuarkusTest
public class DeliveryServiceTest {
 
   @Inject
   DeliveryService deliveryService;

   @Inject @Any
   InMemoryConnector connector;

   @Test
   public void should_process_deliveries() {
       InMemorySource<String> deliveries = connector.source("delivery-warehouse");
       String content = JsonbBuilder.create().toJson(ShoppingBasketObjectMother.theHobbiBasket());
       deliveries.send(content);

       assertThat(deliveryService.getProcessedItems()).hasSize(1);
   }
}
  • Utiliser l'instance InMemorySource pour envoyer des messages au chanel/stream delivery-warehouse.
  • DeliveryService écoute tout message envoyé au chanel/stream delivery-warehouse et le traite.

QuarkusTestResourceLifecycleManager

Nous avons vu que nous pouvons configurer les channels en mémoire dans le fichier application.properties, mais il y a aussi une deuxième façon de le faire en utilisant une approche programmatique. Une QuarkusTestResourceLifecycleManager est nécessaire pour surcharger la configuration du chanel afin d'utiliser le chanel en mémoire. Cette classe a été couverte dans la partie 2 de cette série de billets Tester les applications Web Quarkus : Écrire De Tests De Composants Propres.

La classe InMemoryConnector possède deux méthodes statiques pour configurer les chanels en mémoire, l'une pour l'entrée et l'autre pour la sortie.

public class InMemReactiveMessagingLifecycleManager implements QuarkusTestResourceLifecycleManager {
 
   private Map<String, String> params = new HashMap<>();

   @Override
   public void init(Map<String, String> params) {
       this.params.putAll(params);
   }

   @Override
   public Map<String, String> start() {
       Map<String, String> env = new HashMap<>();
       for (Entry<String, String> con : this.params.entrySet()) {
           switch (con.getValue()) {
               case "incoming": env.putAll(InMemoryConnector.switchIncomingChannelsToInMemory(con.getKey())); break;
               case "outgoing": env.putAll(InMemoryConnector.switchOutgoingChannelsToInMemory(con.getKey())); break;
           }
       }
      
       return env;
   }

   @Override
   public void stop() {
       InMemoryConnector.clear();
   }
}
  • La méthode init est invoquée avant la méthode start et définit les paramètres de configuration dans la classe. Les tuples stockés dans la map sont de la forme <channel name>=<incoming|outgoing>.
  • La map de configuration est itérée pour configurer en conséquence tous les chanels en mémoire.

Puis nous annotons un test avec QuarkusTestResource en définissant l'attribut initArgs avec les chanels requis pour les tests.

@QuarkusTest
@QuarkusTestResource(value = InMemReactiveMessagingLifecycleManager.class, initArgs = {
   @ResourceArg(value = "incoming", name = "delivery-warehouse"),
   @ResourceArg(value = "outgoing", name = "delivery")
})
public class DeliveryServiceTest {}
  • Deux chanels en mémoire sont créés, un chanel entrant nommé delivery-warehouse et un autre chanel sortant nommé delivery. Pendant l'exécution du test, ces chanels sont configurés et prêts à être utilisés.

Test d'intégration de la messagerie réactive

Il est important d'écrire des tests d'intégration pour valider que tout fonctionne dans des circonstances similaires à la production. Pour cet exemple spécifique, nous avons besoin d'un serveur MariaDB et d'un serveur Kafka opérationnels avant l'exécution des tests d'intégration. Comme nous l'avons montré dans la partie 2 Test Des Applications Web Quarkus : Écriture De Tests De Composants Propres, Testcontainers est la meilleure solution ici pour faire tourner ces serveurs à l'intérieur d'un hôte Docker.

Enregistrons les dépendances de Testcontainers pour que les conteneurs MariaDB et Kafka démarrent tous deux au début des tests d'intégration.

Par exemple avec Maven, vous devez ajouter la section suivante dans le pom.xml:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>1.15.1</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>mariadb</artifactId>
  <version>1.15.1</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>kafka</artifactId>
  <version>1.15.1</version>
  <scope>test</scope>
</dependency>

Les tests d'integration

Le CheckoutProcessIntegrationTest vérifie que le processus de paiement se termine correctement -- non pas en s'appuyant sur des dépendances en mémoire mais sur des instances réelles. Ce test utilise un TestProfile personnalisé pour configurer l'application uniquement pour les tests d'intégration.

@QuarkusTest
@TestProfile(IntegrationTestProfile.class)
public class CheckoutProcessIntegrationTest {

   @Test
   public void should_do_a_checkout() throws InterruptedException {

       ShoppingBasket shoppingBasket = ShoppingBasketObjectMother.theHobbiBasket();
       given()
           .contentType(ContentType.JSON)
           .body(shoppingBasket)
           .when()
           .post("/checkout")
           .then()
           .log().ifValidationFails()
           .statusCode(201);
   }
}

Profil du test d'intégration

Comme nous l'avons vu précédemment, pour créer un profil de test, nous devons créer une classe implémentant QuarkusTestProfile. Cette classe doit définir le profil Quarkus spécifique pour les tests d'intégration et enregistrer un QuarkusTestResourceLifecycleManager pour démarrer/arrêter les conteneurs MariaDB et Kafka à l'aide du framework Testcontainers.

public class IntegrationTestProfile implements QuarkusTestProfile {
 
   @Override
   public String getConfigProfile() {
       return "int-test";
   }

   @Override
   public List<TestResourceEntry> testResources() {
       return Collections.singletonList(new TestResourceEntry(InfrastructureTestResource.class));
   }
}
  • int-test est le profil actif lors de l'exécution des tests annotés avec cette classe.
  • InfrastructureTestResource implémente toute la logique nécessaire au démarrage des conteneurs requis.

Ressource pour les tests de Quarkus

La dernière pièce est une classe implémentant le QuarkusTestResourceLifecycleManager qui s'intègre avec Testcontainers et démarre/arrête les conteneurs requis pour les tests d'intégration. Une des caractéristiques importantes de cette classe est que les paramètres de configuration doivent être préfixés par %int-tests car c'est le profil actif lors de l'utilisation du IntegrationTestProfile.

public class InfrastructureTestResource implements QuarkusTestResourceLifecycleManager {

   static MariaDBContainer<?> db = new MariaDBContainer<>("mariadb:10.3.6")
                                           .withDatabaseName("mydb")
                                           .withUsername("developer")
                                           .withPassword("developer");

   static KafkaContainer kafka = new KafkaContainer();

   @Override
   public Map<String, String> start() {
       db.start();
       kafka.start();

       return configurationParameters();
   }

   private Map<String, String> configurationParameters() {
       final Map<String, String> conf = new HashMap<>();
       conf.put("%int-test.quarkus.datasource.jdbc.url", db.getJdbcUrl());
       conf.put("%int-test.quarkus.datasource.username", "developer");
       conf.put("%int-test.quarkus.datasource.password", "developer");
       conf.put("%int-test.quarkus.datasource.db-kind", "mariadb");
       conf.put("%int-test.quarkus.hibernate-orm.database.generation", "drop-and-create");

       conf.put("%int-test.kafka.bootstrap.servers",  kafka.getBootstrapServers());
       conf.put("%int-test.mp.messaging.outgoing.delivery.connector", "smallrye-kafka");
       conf.put("%int-test.mp.messaging.incoming.delivery-warehouse.connector", "smallrye-kafka");
       return conf;
   }

   @Override
   public void stop() {
       if (db != null) {
           db.stop();
       }

       if (kafka != null) {
           kafka.stop();
       }
   }
}
  • Les deux conteneurs MariaDB et Kafka démarrent avant l'exécution de tout test d'intégration.
  • L'application est configurée avec les paramètres des conteneurs MariaDB et Kafka.

Ce test utilise de véritables clusters MariaDB et Kafka au lieu d'une approche en mémoire comme celle utilisée dans les tests des composants.

Code réactif

Quarkus utilise Mutiny comme framework réactif. Lorsque l'on utilise une messagerie réactive, il est facile de se retrouver à utiliser des classes mutiny dans le code. En raison de la nature de la programmation réactive, écrire des tests pour le code réactif n'est pas la chose la plus facile à faire car la plupart des choses se produisent de manière asynchrone. Pour éviter ces complexités, Mutiny fournit une bibliothèque pour attendre et définir des asserts sur les événements.

Le comptage des encaissements

Supposons que nous voulons diffuser en continu toutes les 5 secondes le nombre d'encaissements traités par la plateforme. Le code pourrait ressembler à ceci :

import io.smallrye.mutiny.Multi;

public Multi<Long> totalCheckouts() {
       return Multi.createFrom().ticks().every(Duration.ofSeconds(5))
           .map(tick -> ShoppingBasket.count());
}

Toutes les 5 secondes, le nombre d'éléments traités est compté et envoyé comme un événement dans l'objet Multi créé.

Tests

Enregistrons la dépendance Mutiny Test utils pour commencer à utiliser les classes d'aide qu'elle fournit.
Par exemple dans Maven, vous devez ajouter la section suivante dans le pom.xml :

<dependency>
  <groupId>io.smallrye.reactive</groupId>
  <artifactId>mutiny-test-utils</artifactId>
  <version>0.13.0</version>
  <scope>test</scope>
</dependency>

Le test pour ce cas d'utilisation n'a besoin que de traiter un seul panier, d'attendre que l'événement soit déclenché et d'affirmer que la valeur de l'événement est 1 puisqu'il s'agit du nombre d'articles traités.

import io.smallrye.mutiny.helpers.test.AssertSubscriber;

@Test
public void should_stream_total_checkouts() {

    ShoppingBasket shoppingBasket = ShoppingBasketObjectMother.theHobbiBasket();
    checkoutProcess.checkout(shoppingBasket);

    checkoutProcess.totalCheckouts()
        .transform().byTakingFirstItems(1)
        .subscribe().withSubscriber(AssertSubscriber.create(1))
        .await()
        .assertItems(1L)
        .assertCompleted();
}
  • AssertSubscriber est une classe spéciale fournie par Mutiny pour tester les classes Mutiny. AssertSubscriber.create(1) met 1 comme argument car c'est le nombre d'éléments que le test requiert.
  • la méthode await attend que l'élément soit disponible.
  • Et affirme que la valeur de l'événement est 1 car c'est le nombre total d'encaissements effectués.

Conclusions

Nous avons creusé le sujet des tests Quarkus dans cette série de 3 articles. Des tests de persistance et des tests d'intégration à l'utilisation de conteneurs Docker à des fins de test ou d'applications réactives.

N'oubliez pas que les tests unitaires sont un pilier central pour maintenir la qualité des applications, mais les tests de composants et d'intégration sont également importants et Quarkus excelle dans la prise en charge de ce type de tests.

Le code source est dans le dépôt GitHub.

A propos de l'auteur

Alex Soto est Director of Developer Experience chez Red Hat. Il est passionné par le monde Java, l'automatisation des logiciels, et il croit au modèle du logiciel libre. Alex Soto est le co-auteur de Manning | Testing Java Microservices et O'Reilly | Quarkus Cookbook et contribue à plusieurs projets open-source. Java Champion depuis 2017, il est également conférencier international et enseignant à l'université Salle URL. Vous pouvez le suivre sur Twitter (Alex Soto ⚛️) pour rester au courant de ce qui se passe dans le monde de Kubernetes et de Java.

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT