Il y a 12 ans -
Temps de lecture 6 minutes
Tester les services asynchrones avec Awaitility
Les tests d’intégration impliquent souvent plusieurs composants d’une architecture technique (webservices, serveurs de mail, …). Si une action s’exécute sur un composant A qui fait appel à un composant B et si la condition à vérifier dépend de la bonne exécution de B, vous êtes dans un cas d’asynchronisme. La première idée qui vient à l’esprit est d’utiliser un timer, de mettre en pause l’exécution du test pour laisser le temps à l’action de se réaliser. On comprend très vite qu’une optimisation est à portée de main, et si au lieu d’attendre un temps constant on pouvait gagner du temps si l’action s’est réalisée plus rapidement que prévu.
Nous verrons dans cet article qu’il est possible de développer des tests automatisés capables de s’adapter facilement aux contraintes d’asynchronismes et de se passer d’un blocage à temps constant grâce à l’API Awaitility.
Awaitility est une API open source créée en juillet 2010 pour répondre aux problématiques de tests de systèmes asynchrones. Utilisable sous forme de DSL, elle permet de simplifier les opérations qui nécessitaient auparavant la gestion de Threads, de Time Out et de concurrence d’accès. Awaitility est disponible aussi bien pour Java que pour Groovy et Scala. Le projet est actuellement dans sa version 1.3.1 et a été utilisé dans notre mission pour valider le comportement de deux serveurs qui communiquaient entre eux.
Si vous utilisez Maven, vous pouvez ajouter la dépendance suivante dans votre pom.xml
<dependency> <groupId>com.jayway.awaitility</groupId> <artifactId>awaitility</artifactId> <version>1.3.1</version> <scope>test</scope> </dependency>
Un exemple de test
Imaginons un service chargé de publier des annonces sur des panneaux publicitaires.
Vous êtes un annonceur et vous souhaitez publier une annonce sur l’ensemble des panneaux publicitaires d’un réseau. Un service est mis à votre disposition, voici l’interface :
public interface IServicePanneauxPublicitaires { /** * Propage l'annonce sur tous les panneaux publicitaires La propagation peut durer jusqu'à une minute. * Note : Cet appel est non-bloquant * @param annonce * Annonce à afficher sur les panneaux publicitaires */ void publierAnnonce(String annonce); /** * @return Retourne la liste des panneaux publicitaires */ List<PanneauPublicitaire> getPanneauxPublicitaires(); }
Comme on peut le remarquer, lorsque l’utilisateur de ce service va demander la publication de son annonce, il n’est pas assuré qu’elle sera immédiatement affichée sur l’ensemble du parc de panneaux publicitaires.
Si on désire tester que les panneaux affichent bien l’annonce en question, il faut être capable de laisser une minute (au maximum !) au service pour qu’il puisse faire son travail.
Le test ci-dessous montre comment tirer partie des possibilités d’Awaitility pour construire notre test d’intégration.
@Test /** * Scenario de test : * - publier une annonce via le service IServicePanneauxPublicitaires * - vérifier pour chacun des panneaux que l'annonce est publiée * - contrainte temporelle : la propagation doit durer moins d'une minute */ public void should_propagate_annonce_to_all_panneaux_publicitaires_within_one_minute() throws Exception { // récupération d'une implémentation du service de gestion des panneaux publicitaires IServicePanneauxPublicitaires servicePanneauxPublicitaires = new ServicePanneauxPublicitaires(); // Publication d'une annonce // Rend la main à la méthode appelante mais peut mettre jusqu'à // une minute pour publier l'annonce sur tous les panneaux servicePanneauxPublicitaires.publierAnnonce("Fin des soldes dans 2 jours!"); // Vérification de la publication de l'annonce sur les panneaux Callable<Boolean> condition = isAnnoncePropageeSurTousLesPanneaux(servicePanneauxPublicitaires, "Fin des soldes dans 2 jours!"); Awaitility.await().atMost(Duration.ONE_MINUTE).until(condition); }
Intéressons nous tout d’abord à la dernière ligne car c’est elle qui porte toute la logique du test.
Awaitility.await().atMost(Duration.ONE_MINUTE).until(condition);
En français elle signifie : La condition doit être vérifiée (return true) en moins d’une minute, sinon le test échoue.
La condition en question est un Callable<Boolean> qui contient les éléments de test à valider :
private Callable<Boolean> isAnnoncePropageeSurTousLesPanneaux(final IServicePanneauxPublicitaires servicePanneauxPublicitaires, final String annonce) { return new Callable<Boolean>() { public Boolean call() throws Exception { // pour chaque panneau publicitaire List<PanneauPublicitaire> panneauxPublicitaires = servicePanneauxPublicitaires.getPanneauxPublicitaires(); for (PanneauPublicitaire panneauPublicitaire : panneauxPublicitaires) { // on vérifie que l'annonce a été publiée String annonceCourante = panneauPublicitaire.getAnnonce(); if (!annonce.equals(annonceCourante)) { return false; } } // l'annonce a été propagée sur tous les tableaux return true; } }; }
L’algorithme est simple, pour que le test soit valide, il faut que l’annonce de chaque panneau publicitaire soit égale à l’annonce passée en paramètre. Si au moins un des panneaux n’a pas la bonne annonce, on retourne false. Si tous les panneaux ont la bonne annonce, on retourne true.
Fonctionnement d’Awaitility
Comment se déroule l’exécution du test should_propagate_annonce_to_all_panneaux_publicitaires_within_one_minute ? Awaitility appelle en boucle la méthode isAnnoncePropageeSurTousLesPanneaux() tant que celle-ci retourne false ou que le délai maximum fixé à une minute n’est pas atteint.
L’intérêt d’Awaitility réside dans sa capacité à continuer l’exécution du test dès que la condition est vérifiée. Dans notre cas, si tous les panneaux publicitaires ont été mis à jour en 5 secondes, il n’est pas nécessaire de bloquer le test pendant 1 minute. Par contre, dans le pire des cas, il faudra attendre le time out avant que le test passe en échec.
Mais attention, quand on dit « Awaitility appelle en boucle », il faut comprendre que le callable sera exécuté toutes les 100 ms (valeur par défaut). Cette valeur est modifiable et il est conseillé de la changer si nous ne voulons pas stresser les systèmes à tester.
On désire par exemple appeler le service qu’une fois par seconde, c’est ce qu’Awaitility appelle le pollInterval. On peut fixer le pollInterval pour l’ensemble des tests :
Awaitility.setDefaultPollInterval(Duration.ONE_SECOND); Awaitility.await().atMost(Duration.ONE_MINUTE).until(condition);
Ou simplement pour le test en cours :
Awaitility.await().atMost(Duration.ONE_MINUTE).pollInterval(Duration.ONE_SECOND).until(condition);
De la même manière, on peut modifier le temps d’attente avant que la condition de test soit exécutée pour la première fois. Dans notre cas, on peut laisser 5 secondes d’avance au service de propagation des annonces avant de commencer à l’interroger.
Awaitility.await().atMost(Duration.ONE_MINUTE).pollDelay(Duration.FIVE_SECONDS).until(condition);
Awaitility permet d’associer tous ces paramètres grâce aux méthodes and() et with(). Voici l’exemple complet (pollInterval + pollDelay) en utilisant les imports statiques pour faciliter la lisibilité :
await().atMost(ONE_MINUTE).pollInterval(ONE_HUNDRED_MILLISECONDS).with().pollDelay(TWO_SECONDS).until(condition);
La condition à valider sera vérifiée au bout de 2 secondes et ensuite toutes les 100 millisecondes.
Durée et Unité de temps
Comme nous l’avons vu, Awaitility propose quelques paramètres de temps bien pratiques, voici la liste exhaustive des Duration que vous pouvez utiliser en paramètre de la méthode public ConditionFactory atMost(Duration timeout) :
Duration | Temps |
---|---|
FOREVER | 8 |
ONE_HUNDRED_MILLISECONDS | 100 ms |
TWO_HUNDRED_MILLISECONDS | 200 ms |
FIVE_HUNDRED_MILLISECONDS | 500 ms |
ONE_SECOND | 1 s |
TWO_SECONDS | 2 s |
FIVE_SECONDS | 5 s |
TWO_MINUTES | 2 mn |
FIVE_MINUTES | 5 mn |
TEN_MINUTES | 10 mn |
Vous pouvez bien sûr configurer plus finement le timeout en utilisant la méthode public ConditionFactory atMost(long timeout, TimeUnit unit). Pour rappel, les valeurs de java.util.concurrent.TimeUnit sont :
TimeUnit | Temps |
---|---|
NANOSECONDS | 1/1000 000 000 s |
MICROSECONDS | 1/1000 000 s |
MILLISECONDS | 1/1000 s |
SECONDS | 1 s |
MINUTES | 1 mn |
HOURS | 1 h |
DAYS | 1 j |
Voici un exemple d’utilisation du TimeUnit pour une configuration plus fine des time outs.
// attendre au maximum 3 minutes await().atMost(3, MINUTES).until(condition);
Conclusion
Les possibilités d’Awaitility ne s’arrêtent pas là et vous trouverez de plus amples informations en consultant le site officiel et la Javadoc qui contient des exemples complets.
Commentaire
2 réponses pour " Tester les services asynchrones avec Awaitility "
Published by Piwaï , Il y a 12 ans
Cool, je ne connaissais pas :-) .
La méthode isAnnoncePropageeSurTousLesPanneaux peut gagner en lisibilité avec Guava ;-) . Il suffit de créer un Predicate notPublished, puis de faire un « return Iterables.any(servicePanneauxPublicitaires.getPanneauxPublicitaires(), notPublished); »
Je peux pas m’empêcher de m’interroger sur ce que ça donnerait avec FunkyJFunctional (http://goo.gl/HyHvu), vu que c’est mon dada en ce moment…
=>
class NotPublished extends Pred {{ r = !t.getAnnonce().equals(annonceCourante); }}
Predicate notPublished = withPred(NotPublished.class, this, annonceCourante);
class Condition extends Call {{ r = any(servicePanneauxPublicitaires.getPanneauxPublicitaires(), notPublished); }}
Awaitility.await().atMost(Duration.ONE_MINUTE).until(withCall(Condition.class, this, servicePanneauxPublicitaires, notPublished));
Published by Sebastien , Il y a 12 ans
Je ne suis pas du tout convaincu par l’exemple ci-dessus. Le but c’est d’écrire quelque chose de lisible et compréhensible tout de suite. Il est donc recommandé d’avoir une méthode portant le nom « isAnnoncePropageeSurTousLesPanneaux », peut importe son implémentation.
Ici, l’oeil lit « until is annonce propagee », pas besoin de passer un quart d’heure à comprendre …
Awaitility.await().atMost(Duration.ONE_MINUTE)
.until(isAnnoncePropageeSurTousLesPanneaux(
servicePanneauxPublicitaires, « Fin des soldes dans 2 jours! »));