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 : Écriture De Tests De Composants Propres

Test Des Applications Web Quarkus : Écriture De Tests De Composants Propres

Favoris

Points Clés

  • Quarkus est un framework Java Kubernetes-native full-stack conçu pour les machines virtuelles Java (JVM) et la compilation native. Bien que de nombreuses techniques de test restent les mêmes, Quarkus fournit des technologies de support pour faciliter la configuration et l'exécution des tests.
  • Dans cet article, nous allons apprendre à rédiger des tests d'intégration propres pour les applications Quarkus. Nous verrons comment nous pouvons écrire des tests simples et propres pour les scénarios suivants : un client de messagerie, la sécurité avec RBAC, des tests à l'aide de conteneurs et des clients REST.
  • Quarkus dispose d'un module Quarkus Test Security qui permet une modification déterministe du contexte de sécurité pendant la phase de test.
  • Testcontainers est une bibliothèque Java qui permet de démarrer/arrêter des conteneurs Docker par programmation à partir du code Java. Il prend en charge les conteneurs de base de données les plus utilisés, Kafka, Localstack ou WebDrivers, pour n'en nommer que quelques-uns.
  • La virtualisation de service est une technique utilisée pour simuler le comportement des dépendances d'un service. Bien que la virtualisation des services soit généralement associée aux services basés sur des API REST, le même concept peut être appliqué à tout autre type de dépendances telles que les bases de données, les ESB et JMS.

 

Quarkus est un framework Java Kubernetes-native 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 rend compilables en binaire à l'aide de Graal VM.

Dans cet article, nous allons apprendre à rédiger des tests d'intégration propres pour les applications Quarkus. Nous verrons comment nous pouvons écrire des tests simples et propres pour les scénarios suivants :

  • Client mail
  • Sécurité avec RBAC
  • Test à l'aide de conteneurs
  • Clients REST

Voyons comment nous pouvons écrire des tests.

L'application

Nous utiliserons la même application que dans la partie 1 de cet article situé ici.

Pour rappel, l'application est un simple service d'enregistrement des utilisateurs développé dans Quarkus, et il est composé des classes suivantes :

Client mail

Ajoutons une nouvelle exigence lorsqu'un nouvel utilisateur est enregistré. Supposons que chaque fois qu'un nouvel utilisateur est enregistré, l'application doit envoyer un e-mail à l'utilisateur avec le mot de passe généré automatiquement. La logique de mise en œuvre de ce cas d'utilisation utilise l'extension quarkus-mailer et elle est implémentée dans un bean CDI :

import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;

@ApplicationScoped
public class MailService {
  
   @Inject
   Mailer mailer;

   public void sendEmail(final User user) {
       mailer.send(
           Mail.withText(user.email, "Your New Password",
                           String.format("New Password %s.", user.password))
       );
   }
}

Maintenant, nous devons écrire un test pour cette classe. L'une des approches qui pourraient vous venir à l'esprit est d'utiliser Mockito comme nous l'avons vu dans l'article précédent, et c'est un bon point, mais Quarkus propose un client de messagerie stubbed qui est injecté automatiquement dans votre base de code avec les profiles dev et test. Puisqu'il est stubbed, vous pouvez l'interroger pour obtenir le nombre total de messages envoyés, obtenir tous les messages envoyés par un utilisateur ou les effacer.

Comme ce client de messagerie stubbed est automatiquement utilisé dans le profil test, nous pouvons injecter une instance de io.quarkus.mailer.MockMailbox dans notre test et l'utiliser dans la section assertions. Dans l'extrait de code suivant, vous voyez à quoi ressemble un test de MailService :

@QuarkusTest
public class MailServiceTest {
  
   @Inject
   MockMailbox mailbox;

   @Inject
   MailService mailService;

   @BeforeEach
   void init() {
       mailbox.clear();
   }

   @Test
   public void shouldSendAnEmail() {
       User user = new User("Alex", "alex@example.com", "abcd");

       mailService.sendEmail(user);

       assertThat(mailbox.getMessagesSentTo("alex@example.com"))
           .hasSize(1)
           .extracting("subject")
           .containsExactly("Your New Password");

   }
}
  • MockMailbox est la classe dans laquelle tous les e-mails sont envoyés. Aucun vrai serveur de mail n'est utilisé.
  • MailService est injecté car il s'agit de la logique métier testée.
  • Avant chaque exécution de test, la boîte aux lettres est nettoyée afin que les tests soient isolés.
  • mailbox.getMessagesSentTo("alex@example.com") renvoie une liste de tous les e-mails envoyés pour l'adresse alex@example.com.

INFO : tous les messages sont affichés sur le terminal Quarkus lorsque MockMailbox est activé.

CONSEIL : Vous pouvez désactiver l'injection de MockMailbox dans les profiles dev et test en définissant la propriété de configuration quarkus.mailer.mock à false.

Sécurité et RBAC

Jusqu'à présent, nous avons accédé librement aux endpoints REST sans aucun mécanisme d'authentification. Bien que cela puisse fonctionner pour certains endpoints, d'autres (en particulier ceux liés aux tâches d'administration) doivent être protégés.

Quarkus Security s'intègre au modèle de sécurité JavaEE (c'est-à-dire l'annotation javax.annotation.security.RolesAllowed) et fournit plusieurs mécanismes d'authentification et d'autorisation à utiliser. Pour en citer quelques-uns : OpenId Connect, JWT, Basic Auth, OAuth 2, JDBC ou LDAP.

Protégeons le endpoint findUserByUsername pour limiter l'accès au rôle Admin, car c'est quelque chose qu'un utilisateur ordinaire ne devrait jamais faire :

@GET
@RolesAllowed("Admin")
@Path("/{username}")
@Produces(MediaType.APPLICATION_JSON)
public Response findUserByUsername(@PathParam("username") String username) {
}

Rédiger un test en boîte blanche

Il est maintenant temps d'écrire un test en boîte blanche à l'aide de RestAssured pour valider que nous pouvons trouver un utilisateur par son nom d'utilisateur. (Nous l'avons déjà fait dans la partie 1 de cet article.) Le test de ce endpoint est présenté dans l'extrait de code suivant :

@Test
@Order(2)
public void shouldFindAUserByUsername() {
   given()
      .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
      .when().get("/{username}", "Alex")
      .then()
         .statusCode(200)
         .body("username", is("Alex"))
         .body("email", is("asotobu@example.com"))
         .body("password", is("my-secret-password"));
  • Il existe une méthode de test exécutée avant ce test qui insère un utilisateur.
  • RestAssured est utilisé pour appeler le endpoint et valider également la réponse.

Mais que se passe-t-il lorsque nous exécutons cette logique ? Exactement! Il échoue car le code d'état n'est pas 200 OK mais 401 Unauthorized car le endpoint est protégé et nous n'avons pas été authentifiés dans le système.

Donc, à ce stade, nous avons deux options - la première est de s'authentifier dans le système. Cela pourrait être une bonne approche mais :

  1. La logique d'authentification par rapport au système n'est peut-être pas facile. Pensez, par exemple, dans le cas d'OAuth2. Pour exécuter le test, nous aurions besoin d'un fournisseur d'identité tel que Keycloak configuré avec certaines données de test, et le test doit exécuter une logique pour s'authentifier auprès du fournisseur d'identité en suivant le protocole de sécurité.
  2. Le test doit s'exécuter rapidement. Il s'exécute désormais beaucoup plus lentement car il y a une surcharge dans la préparation du test.
  3. Toute modification du mécanisme d'authentification affecte tous les tests et ajoute une exigence/un prérequis.

Évidemment, nous devons tester si le véritable mécanisme de sécurité fonctionne, mais cela peut être mis en œuvre dans des tests de sécurité spécifiques, et non dans des tests où nous validons la logique métier. Pour cette raison, explorons une deuxième option.

Tester la sécurité

Quarkus dispose d'un module Quarkus Test Security qui vous permet de modifier le contexte de sécurité pendant la phase de test.

Pour utiliser le module Quarkus Test Security, nous devons ajouter la dépendance quarkus-test-security dans notre script de l'outil de build. Par exemple, dans Maven, vous devez ajouter la section suivante dans le pom.xml :

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-test-security</artifactId>
  <scope>test</scope>
</dependency>

Ce module fournit l'annotation io.quarkus.test.security.TestSecurity pour contrôler le contexte de sécurité avec lequel le test est exécuté. Fondamentalement, vous pouvez soit contourner l'autorisation pour que les tests puissent accéder aux endpoints sécurisés sans avoir besoin d'être authentifié, et/ou vous pouvez spécifier l'utilisateur et les rôles utilisés dans les tests.

Réécrivons le test précédent avec l'autorisation désactivée.

@Test
@Order(2)
@TestSecurity(authorizationEnabled = false)
public void shouldFindAUserByUsername() {
  ...
}
  • Le test peut accéder aux endpoints sécurisés sans avoir besoin de s'authentifier.

Si nous réexécutons maintenant le test, celui-ci réussit car la sécurité est désactivée pour ce test spécifique.

Nous pouvons également utiliser la même annotation pour configurer l'utilisateur/les rôles sous lesquels le test sera exécuté :

@Test
@Order(2)
@TestSecurity(user = "john", roles = "Admin")
public void shouldFindAUserByUsername() {
   ...
}

Si nous exécutons à nouveau le test, il réussit car l'utilisateur et le rôle correspondent aux contraintes de sécurité. Changez l'attribut roles de Admin à Admin2, et le test échouera à cause d'un problème de sécurité.

Les callbacks

Quarkus dispose d'un mécanisme d'extension pour enrichir toutes vos classes annotées avec @QuarkusTest en implémentant les interfaces de callback suivantes :

  • io.quarkus.test.junit.callback.QuarkusTestBeforeClassCallback exécute les traitements avant toute exécution de la classe de test.
  • io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback exécute les traitements avant chaque exécution d'une méthode de test.
  • io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback exécute les traitements après chaque exécution d'une méthode de test.

Créons un callback qui est exécuté avant toute méthode de test, imprimant la méthode de test actuelle et toutes les annotations placées au niveau de la classe.

public class MyQuarkusTestBeforeEachCallback implements QuarkusTestBeforeEachCallback {

   @Override
   public void beforeEach(QuarkusTestMethodContext context) {
       System.out.println("Executing " + context.getTestMethod());

       Annotation[] annotations = context.getTestInstance().getClass().getAnnotations();
       Arrays.stream(annotations)
           .forEach(System.out::println);
   }
}
  • Nous pouvons obtenir la méthode de test actuelle en utilisant l'objet context.
  • L'autre élément que nous pouvons obtenir est l'instance de test actuelle. Avec l'objet instance, nous pouvons injecter des attributs à l'instance de test, lire des valeurs, ou lire des méta-informations statiques comme les annotations.

Les callbacks de test Quarkus doivent être enregistrés en tant que Java service provider. Créez le fichier suivant :

src/main/resources/META-INF/services/io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback 

Avec le contenu ci-dessous :

org.acme.MyQuarkusTestBeforeEachCallback

Puis pour chaque test, org.acme.MyQuarkusTestBeforeEachCallback est exécuté.

IMPORTANT:             Bien qu'il soit possible d'utiliser les interfaces de callback de JUnit Jupiter, vous risquez de rencontrer des problèmes de chargement de classe car Quarkus doit exécuter les tests dans un classloader personnalisé que JUnit ne connaît pas.

Les callbacks sont un bon moyen d'exécuter certains traitements avant/après chaque test de manière réutilisable. Un bon cas d'utilisation des callbacks pourrait être l'encapsulation de la logique d'authentification pour vos tests de bout en bout. Au lieu de répéter la logique d'authentification dans chaque classe de test, nous pourrions simplement la déléguer à un callback.

import org.acme.api.OpenIdAuthentication;

public class OpenIdAuthenticationTestBeforeClassCallback implements QuarkusTestBeforeClassCallback {

   @Override
   public void beforeClass(Class<?> testClass) {
      
       OpenIdAuthentication openIdAuth = testClass.getAnnotation(OpenIdAuthentication.class);

       if (openIdAuth != null) {

           String accessToken = getToken(openIdAuth);

           final RequestSpecBuilder requestSpec = new RequestSpecBuilder();
           requestSpec.addHeader("Authorization", "Bearer " + accessToken);
          
           RestAssured.requestSpecification = requestSpec.build();
       }
      
   }  
}

Et il suffit alors d'annoter les tests avec OpenIdAuthentication.

@OpenIdAuthentication(username = "Ada", password = "Alexandra")
@QuarkusTest
public class RegeneratePasswordTest {
}

ASTUCE: les callbacks ne sont pas CDI-aware donc vous ne pouvez pas injecter un bean CDI dans la classe. Si vous devez le faire, vous pouvez toujours compter sur la recherche programmatique via la méthode io.quarkus.arc.Arc.container().

Avec les callbacks, nous pouvons implémenter une logique de test réutilisable qui est exécutée avant ou après les tests, mais les callbacks ne couvrent pas tous les cas d'utilisation possibles que nous pouvons trouver lors du développement des tests. L'un de ces besoins courants est d'exécuter certains traitements avant le démarrage de l'application Quarkus, de reconfigurer Quarkus avec des propriétés de configuration spécifiques, et d'exécuter certains traitements avant l'arrêt de l'application Quarkus. Par exemple, démarrer un conteneur Docker en utilisant le projet Testcontainers (https://www.testcontainers.org/), utiliser le conteneur pendant l'exécution du test, et à la fin l'arrêter.

Voyons donc comment y parvenir dans les tests Quarkus.

Quarkus Test Resource

Quarkus dispose d'un mécanisme permettant d'exécuter certains traitements avant que l'application ne soit en marche et lorsqu'elle est arrêtée, ainsi que de reconfigurer l'application avec de nouvelles valeurs pendant l'exécution du test.

Il nous suffit de créer une classe implémentant io.quarkus.test.common.QuarkusTestResourceLifecycleManager et d'annoter un test de la suite de tests avec @QuarkusTestResource.

Si plusieurs ressources de test Quarkus sont définies, @QuarkusTestResource possède l'attribut parallel pour les lancer simultanément.

Testcontainers

Testcontainers est une bibliothèque Java qui permet de démarrer/arrêter des conteneurs Docker de manière programmatique à partir du code Java. Elle prend en charge les conteneurs de base de données les plus utilisés, Kafka, Localstack ou WebDrivers, pour n'en citer que quelques-uns.

Pour utiliser les Testcontainers, nous devons ajouter les dépendances testcontainers dans le script de notre outil de build. Par exemple, dans Maven, vous devez ajouter la section suivante dans pom.xml :

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

Tests d'intégration de la persistance

Écrivons un test d'intégration de la persistance, mais au lieu d'utiliser une base de données en mémoire intégrée comme dans la première partie, nous utilisons une base de données MariaDB Dockerisée.

La première chose à faire est de créer une classe implémentant l'interface QuarkusTestResourceLifecycleManager. Cette classe démarre/arrête une instance MariaDB Dockerisée et configure la source de données Quarkus pour l'utiliser.


import org.testcontainers.containers.MariaDBContainer;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

public class MariaDBResource implements QuarkusTestResourceLifecycleManager {

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

   @Override
   public Map<String, String> start() {
       db.start();
      
       final Map<String, String> conf = new HashMap<>();
       conf.put("%test.quarkus.datasource.jdbc.url", db.getJdbcUrl());
       conf.put("%test.quarkus.datasource.username", "developer");
       conf.put("%test.quarkus.datasource.password", "developer");
       conf.put("%test.quarkus.datasource.db-kind", "mariadb");
      
       return conf;

   }

   @Override
   public void stop() {
      db.stop();
   }  
}
  • la classe MariaDBContainer encapsule toute la logique pour traiter le cycle de vie des conteneurs MariaDB.
  • Une Map est retournée avec la nouvelle configuration de la source de données utilisée par l'application Quarkus.

La deuxième étape consiste à annoter un de nos tests avec l'annotation io.quarkus.test.common.QuarkusTestResource. Il est important de remarquer que bien qu'une seule classe soit annotée, la logique n'est exécutée qu'une seule fois avant le démarrage de l'application donc la logique n'est exécutée qu'une seule fois pour toute la suite de tests.

@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestHTTPEndpoint(RegistrationResource.class)
@QuarkusTestResource(MariaDBResource.class)
public class RegistrationResourceTest {

   @TestHTTPResource
   @TestHTTPEndpoint(RegistrationResource.class)
   URL url;

   @InjectMock
   PasswordGenerator passwordGenerator;

   @Test
   @Order(1)
   public void shouldRegisterAUser() {
      
       Mockito.when(passwordGenerator.generate()).thenReturn("my-secret-password");

       final User user = new User();
       user.username = "Alex";
       user.email = "asotobu@example.com";

       given()
         .body(user)
         .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
         .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
         .when().post()
         .then()
            .statusCode(Status.CREATED.getStatusCode())
            .header("location", url + "/1");
   }
}

L'implémentation du QuarkusTestResourceLifecycleManager est définie sur l'annotation QuarkusTestResource.

Jan 18, 2021 5:06:25 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.MariaDB103Dialect
2021-01-18 17:06:14,283 INFO  [org.tes.doc.DockerClientProviderStrategy] (main) Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
2021-01-18 17:06:15,043 INFO  [org.tes.doc.DockerClientProviderStrategy] (main) Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
2021-01-18 17:06:15,044 INFO  [org.tes.DockerClientFactory] (main) Docker host IP address is localhost
2021-01-18 17:06:15,085 INFO  [org.tes.DockerClientFactory] (main) Connected to docker:
  Server Version: 19.03.8
  API Version: 1.40
  Operating System: Docker Desktop
  Total Memory: 7964 MB
2021-01-18 17:06:15,088 INFO  [org.tes.uti.ImageNameSubstitutor] (main) Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2021-01-18 17:06:15,841 INFO  [org.tes.DockerClientFactory] (main) Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
2021-01-18 17:06:15,841 INFO  [org.tes.DockerClientFactory] (main) Checking the system...
2021-01-18 17:06:15,842 INFO  [org.tes.DockerClientFactory] (main) ✔︎ Docker server version should be at least 1.6.0
2021-01-18 17:06:15,955 INFO  [org.tes.DockerClientFactory] (main) ✔︎ Docker environment should have more than 2GB free disk space
2021-01-18 17:06:16,125 INFO  [ .3.6]] (main) Creating container for image: mariadb:10.3.6
2021-01-18 17:06:16,282 INFO  [ .3.6]] (main) Starting container with ID: e7206ee3b7f529526e0207321df3ea67487d8ab0e652bba9d6bc0200bc9bd61a
2021-01-18 17:06:16,543 INFO  [ .3.6]] (main) Container mariadb:10.3.6 is starting: e7206ee3b7f529526e0207321df3ea67487d8ab0e652bba9d6bc0200bc9bd61a
2021-01-18 17:06:16,555 INFO  [ .3.6]] (main) Waiting for database connection to become available at jdbc:mariadb://localhost:32791/mydb using query 'SELECT 1'
2021-01-18 17:06:24,613 INFO  [ .3.6]] (main) Container is started (JDBC URL: jdbc:mariadb://localhost:32791/mydb)
2021-01-18 17:06:24,613 INFO  [ .3.6]] (main) Container mariadb:10.3.6 started in PT8.651928S

La partie importante de ces lignes est qu'un conteneur Docker MariaDB est démarré automatiquement. Ensuite, des tests sont exécutés en utilisant cette instance comme base de données et enfin, l'instance est arrêtée.

Ressources de test Quarkus fournies out-of-the-box

Certaines des extensions Quarkus fournissent des ressources de test Quarkus déjà implémentées que nous pouvons utiliser dans nos tests. Les plus importantes sont présentées dans le tableau ci-dessous :

Objectif

Dépendance

Quarkus Test Resource

Description

SQL

io.quarkus:quarkus-test-h2

H2DatabaseTestResource

Démarre une instance H2 locale en mode serveur

LDAP

io.quarkus:quarkus-test-ldap

LdapServerTestResource

Démarre un LDAP local en mémoire. Avec dc=quarkus,dc=io et des informations d'identification de liaison ("uid=admin,ou=system", "secret"). Importe LDIF depuis un fichier situé à la racine du classpath nommé quarkus-io.ldif.

Kubernetes

io.quarkus:quarkus-test-kubernetes-client

KubernetesMockServerTestResource

Démarre une copie locale du serveur Kubernetes API et définit les variables d'environnement nécessaires au client Kubernetes.

Vous pouvez enregistrer les attentes en injectant l'instance suivante dans votre test :

@MockServer private KubernetesMockServer mockServer;

Vault

io.quarkus:quarkus-test-vault

VaultTestLifecycleManager

Démarre une instance de Vault d'Hashicorp

JMS

io.quarkus:quarkus-test-artemis

ArtemisTestResource

Démarre une instance locale embarquée d'Artemis.

SQL

io.quarkus:quarkus-test-derby

DerbyDatabaseTestResource

Démarre une instance locale de Derby

Nous n'avons couvert que les tests de persistance, mais nous pouvons utiliser les ressources de test Quarkus pour toutes les dépendances dont nos tests pourraient avoir besoin, comme un cluster Kafka, un serveur de messagerie ou un service développé par une autre équipe.

À ce stade, nous savons comment écrire des tests de persistance d'intégration en utilisant le même serveur de base de données que celui utilisé en production, mais il nous reste un cas d'utilisation typique dans l'architecture de services qui n'a pas encore été couvert dans l'article. Comment tester la communication entre les services ? Nous allons voir comment le faire dans la section suivante.

Client REST Client

Ajoutons une nouvelle restriction à notre service d'enregistrement. Supposons qu'il existe un service qui vérifie si un nom d'utilisateur est interdit d'utilisation (surnoms offensifs, caractères invalides, ...) et que nous devons l'appeler avant qu'un nouvel utilisateur soit inséré dans notre système pour éviter toute violation des règles. Dans la figure ci-dessous, vous pouvez voir à quoi ressemble le système :

Le service des utilisateurs bannis a un endpoint simple qui renvoie true si un nom d'utilisateur est invalide ou false sinon. Le endpoint est un GET /api/<username>.

Quarkus s'intègre à la spécification MicroProfile Rest Client par le biais de l'extension quarkus-rest-client pour fournir une approche sécurisée en termes de type pour invoquer des services RESTful sur HTTP. Le but de cet article n'est pas d'expliquer comment développer un Rest Client dans Quarkus, donc seules les parties importantes pour les tests sont montrées. Implémentons la logique :

La première chose à faire est de créer une interface de mapping des interactions REST.

@Path("/api")
@RegisterRestClient
@ApplicationScoped
public interface BannedUserClient {
   @GET @Path("/{username}")
   @Produces(MediaType.TEXT_PLAIN)
   String isBanned(@PathParam("username") String username);
}

Ensuite, un bean CDI est créé pour envelopper l'interaction avec le service externe, bien que dans ce cas, ce soit assez simple, dans d'autres cas, cela peut nécessiter une logique plus compliquée.

@ApplicationScoped
public class BannedUserService {
  
   @RestClient
   BannedUserClient bannedUserClient;

   public boolean isBanned(String username) {
       String banned = bannedUserClient.isBanned(username);
       return Boolean.valueOf(banned);
   }
}

Le endpoint d'enregistrement est mis à jour avec ces changements :

@Path("/registration")
public class RegistrationResource {

   @Inject
   BannedUserService bannedUserService;
   
   @POST
   @Transactional
   @Consumes(MediaType.APPLICATION_JSON)
   public Response insertUser(User user) {   
      
      if (bannedUserService.isBanned(user.username)) {
         return Response.status(Status.PRECONDITION_FAILED.getStatusCode())
.build();
      } else {
         …
      }
}

Enfin, le nom d'hôte du service des utilisateurs bannis est configuré dans le fichier application.properties.

org.acme.BannedUserClient/mp-rest/url=http://banned-user-service

A ce stade, nous devons mettre à jour le RegistrationResourceTest avec les modifications introduites. Et nous avons 4 options possibles :

  • Ne changez rien et exécutez le test contre une instance déjà en cours d'exécution de Banned User Service. C'est la méthode la plus simple, pourtant celle qui peut vous conduire à des tests bancals si le service a un temps d'arrêt.
  • Démarrez une instance locale de Banned User Service. C'est une stratégie juste et pourrait fonctionner dans des services simples, mais si le service a des dépendances sur d'autres services ou bases de données, alors il pourrait être difficile d'exécuter et de maintenir ces tests.
  • Mocker le client REST. C'est la stratégie la plus utilisée car elle est facile à utiliser et n'a pas de dépendance externe.
  • Utiliser l'outillage de la virtualisation de services pour ne pas simuler au niveau de l'objet mais au niveau du réseau.

Les deux dernières options sont les plus utilisées lors des tests d'architecture de services, voyons donc comment les mettre en œuvre dans les tests Quarkus :

Les Mocks

Nous avons vu les mocks dans la première partie de cette série sur les tests Quarkus, mais la simulation de l'interface du client Rest nécessite un petit changement.

Rappelez-vous que pour utiliser Mockito, nous devons enregistrer la dépendance io.quarkus:quarkus-junit5-mockito dans l'outil de build.

La plus grande différence lors du mocking de l'interface du client Rest par rapport au mocking d'un simple bean CDI est que l'annotation org.eclipse.microprofile.rest.client.inject.RestClient est nécessaire ainsi que io.quarkus.test.junit.mockito.InjectMock.

Écrivons un test qui vérifie que lorsqu'un nom d'utilisateur est invalide, un code de statut 421 Precondition Failed est renvoyé.

@QuarkusTest
@TestHTTPEndpoint(RegistrationResource.class)
@QuarkusTestResource(MariaDBResource.class)
public class RegistrationResourceTest {
   @InjectMock
   @RestClient
   BannedUserClient bannedUserClient;

   @Test
   public void shouldNotAddAUserIfBannedName() {
     Mockito.when(bannedUserClient.isBanned("Alex")).thenReturn("true");

     final User user = new User();
     user.username = "Alex";
     user.email = "asotobu@example.com";

     given()
       .body(user)
       .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
       .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
       .when().post()
       .then()
         .statusCode(Status.PRECONDITION_FAILED.getStatusCode());
   }
} 
  • org.eclipse.microprofile.rest.client.inject.RestClient et io.quarkus.test.junit.mockito.InjectMock sont utilisés ensemble pour injecter un mock de l'interface du client Rest.
  • org.mockito.Mockito classe est utilisée comme nous le faisons habituellement.

Le principal avantage de cette approche est qu'elle est vraiment facile à mettre en œuvre sans aucune pénalité de performance. Cela fonctionne dans la plupart de nos tests, mais lorsque nous écrivons des tests d'intégration pour le client Rest, nous ne voulons pas simuler l'interface car ce que nous voulons vraiment, c'est tester la pile d'appels complète, du code au réseau et inversement. L'utilisation du service réel peut ne pas être une option pour les raisons que nous avons vues précédemment, c'est alors que la virtualisation de service entre en scène :

la virtualisation de service

La virtualisation de services est une technique utilisée pour simuler le comportement des dépendances d'un service. Bien que la virtualisation de services soit généralement associée aux services basés sur l'API REST, le même concept peut être appliqué à tout autre type de dépendances comme les bases de données, les ESB, JMS, ...

La virtualisation de services crée un serveur proxy où toute demande est interceptée et où une réponse standard est fournie à l'appelant. Du point de vue du service testé, la demande est envoyée à un vrai serveur, de sorte que toute la pile est testée.

Voyons comment nous pouvons utiliser la virtualisation des services avec Quarkus.

Hoverfly

Hoverfly  est un outil de simulation de virtualisation de services d'API, léger et open source, écrit avec le langage de programmation Go. Il offre également des liaisons de langage qui s'intègrent étroitement à Java (https://docs.hoverfly.io/projects/hoverfly-java/en/latest/).

Pour utiliser le Hoverfly, nous devons ajouter les dépendances Hoverfly dans le script de notre outil de build. Par exemple, dans Maven, vous devez ajouter la section suivante dans le pom.xml :

<dependency>
  <groupId>io.specto</groupId>
  <artifactId>hoverfly-java</artifactId>
  <version>0.14.0</version>
  <scope>test</scope>
</dependency>

Développons ensuite une ressource de test Quarkus qui démarre/arrête le serveur proxy Hoverfly et enregistre quelques réponses fixes.


import static io.specto.hoverfly.junit.core.HoverflyConfig.localConfigs;
import static io.specto.hoverfly.junit.core.SimulationSource.dsl;
import static io.specto.hoverfly.junit.dsl.HoverflyDsl.service;
import static io.specto.hoverfly.junit.dsl.ResponseCreators.success;
import static io.specto.hoverfly.junit.core.HoverflyMode.SIMULATE;
import io.specto.hoverfly.junit.core.Hoverfly;

public class HoverflyResource implements QuarkusTestResourceLifecycleManager {

   private Hoverfly hoverfly;

   @Override
   public Map<String, String> start() {
       hoverfly = new Hoverfly(localConfigs().destination("banned-user-service"), SIMULATE);

       hoverfly.start();
       hoverfly.simulate(
           dsl(
               service("banned-user-service")
               .get("/api/Alex")
               .willReturn(success("true", MediaType.TEXT_HTML))
               .get("/api/Ada")
               .willReturn(success("false", MediaType.TEXT_HTML))
           )
       );
      
       return null;
   }

   @Override
   public void stop() {
       hoverfly.close();
   }
}
  • La classe Hoverfly est utilisée pour contrôler le cycle de vie du proxy du serveur.
  • La méthode service définit le nom d'hôte sous simulation. C'est l'hôte configuré dans application.properties.
  • Deux interactions sont enregistrées, l'une renvoyant le nom d'utilisateur qui est banni et l'autre non.

Maintenant, nous pouvons supprimer la référence de mocking du test RegistrationResourceTest et l'annoter avec HoverflyResource.

@QuarkusTest
@TestHTTPEndpoint(RegistrationResource.class)
@QuarkusTestResource(MariaDBResource.class)
@QuarkusTestResource(HoverflyResource.class)
public class RegistrationResourceTest {

   @InjectMock
   PasswordGenerator passwordGenerator;

   @Test
   public void shouldNotAddAUserIfBannedName() {

     final User user = new User();
     user.username = "Alex";
     user.email = "a@example.com";

     given()
       .body(user)
       .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
       .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON)
       .when().post()
       .then()
         .statusCode(Status.PRECONDITION_FAILED.getStatusCode());
   }
}

Il n'y a pas de code de mocking, le test suppose simplement qu'un service distant est en place et fonctionne. En fait, il y en a un - pas le vrai Banned Users Service mais un service proxy. En inspectant la console, vous verrez que Hoverfly fonctionne :

2021-01-19 14:55:52,716 INFO  [hoverfly] (Thread-43) Default proxy port has been overwritten port=55949
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Default admin port has been overwritten port=55950
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Using memory backend 
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Proxy prepared... Destination=. Mode=simulate ProxyPort=55949
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) current proxy configuration destination=. mode=simulate port=55949
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) serving proxy 
2021-01-19 14:55:52,717 INFO  [hoverfly] (Thread-43) Admin interface is starting... AdminPort=55950
2021-01-19 14:55:52,776 INFO  [io.spe.hov.jun.cor.Hoverfly] (main) A local Hoverfly with version v1.3.1 is ready
2021-01-19 14:55:52,782 INFO  [hoverfly] (Thread-43) Mode has been changed mode=simulate

Hoverfly est lancé et les réponses simulées sont enregistrées. Remarquez que ce test a deux ressources de test Quarkus enregistrées, une pour MariaDB et une autre pour Hoverfly.

Conclusions

Nous nous sommes penchés sur les tests Quarkus, sur la manière de contourner les contraintes de sécurité à des fins de test, sur l'utilisation de Testcontainers pour écrire des tests d'intégration et, enfin, sur la manière de tester lorsqu'un service dépend d'un autre service.

Mais il y a encore quelques trucs et astuces de test dans Quarkus qui ne sont pas encore couverts. Par exemple, comment tester du code réactif/synchrone lorsqu'on utilise Kafka ou lorsque des tâches périodiques sont définies. Nous couvrirons ces sujets dans la troisième partie de cet article.

Code source

About the Author

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 est le co-auteur des livres Testing Java Microservices et Quarkus cookbook et contributeur à 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 (@alexsotob) pour rester à l'écoute de ce qui se passe dans le monde de Kubernetes et de Java.

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

BT