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 :
- Le panier est reçu par le service de paiement.
- Un appel HTTP synchrone est effectué vers le service de remise pour appliquer une éventuelle remise.
- L'achat est stocké dans la base de données.
- Un événement est émis vers un topic Kafka.
- L'événement est reçu de manière asynchrone par le service de livraison.
- 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 :
- l'appel du service de remise est effectué pour appliquer toute remise sur le prix final.
- les données sont conservées dans la base de données.
- 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 testsprod
: 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 deQuarkusTestResourceLifecycleManager
.disableGlobalTestResources
renvoie true si seules les ressources de test renvoyées partestResources
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 channeldelivery
. - 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/streamdelivery-warehouse
. DeliveryService
écoute tout message envoyé au chanel/streamdelivery-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.