Introduction à Awaitility

1. Introduction

Un problème courant avec les systèmes asynchrones est qu'il est difficile d'écrire pour eux des tests lisibles qui sont axés sur la logique métier et ne sont pas pollués par les synchronisations, les délais d'expiration et le contrôle de la concurrence.

Dans cet article, nous allons jeter un œil à Awaitility - une bibliothèque qui fournit un simple langage spécifique au domaine (DSL) pour les tests de systèmes asynchrones .

Avec Awaitility, nous pouvons exprimer nos attentes vis-à-vis du système dans un DSL facile à lire.

2. Dépendances

Nous devons ajouter des dépendances Awaitility à notre pom.xml.

La bibliothèque d' attente sera suffisante pour la plupart des cas d'utilisation. Si nous voulons utiliser des conditions basées sur un proxy , nous devons également fournir la bibliothèque awaitility-proxy :

 org.awaitility awaitility 3.0.0 test   org.awaitility awaitility-proxy 3.0.0 test 

Vous pouvez trouver la dernière version des bibliothèques Waitility et Waitility-proxy sur Maven Central.

3. Création d'un service asynchrone

Écrivons un service asynchrone simple et testons-le:

public class AsyncService { private final int DELAY = 1000; private final int INIT_DELAY = 2000; private AtomicLong value = new AtomicLong(0); private Executor executor = Executors.newFixedThreadPool(4); private volatile boolean initialized = false; void initialize() { executor.execute(() -> { sleep(INIT_DELAY); initialized = true; }); } boolean isInitialized() { return initialized; } void addValue(long val) { throwIfNotInitialized(); executor.execute(() -> { sleep(DELAY); value.addAndGet(val); }); } public long getValue() { throwIfNotInitialized(); return value.longValue(); } private void sleep(int delay) { try { Thread.sleep(delay); } catch (InterruptedException e) { } } private void throwIfNotInitialized() { if (!initialized) { throw new IllegalStateException("Service is not initialized"); } } }

4. Test avec attente

Maintenant, créons la classe de test:

public class AsyncServiceLongRunningManualTest { private AsyncService asyncService; @Before public void setUp() { asyncService = new AsyncService(); } //... }

Notre test vérifie si l'initialisation de notre service se produit dans un délai spécifié (par défaut 10s) après l'appel de la méthode initialize .

Ce cas de test attend simplement que l'état d'initialisation du service change ou lève une exception ConditionTimeoutException si le changement d'état ne se produit pas.

L'état est obtenu par un appelable qui interroge notre service à des intervalles définis (100 ms par défaut) après un délai initial spécifié (100 ms par défaut). Ici, nous utilisons les paramètres par défaut pour le délai d'expiration, l'intervalle et le délai:

asyncService.initialize(); await() .until(asyncService::isInitialized);

Ici, nous utilisons await - l'une des méthodes statiques de la classe Awaitility . Il renvoie une instance d'une classe ConditionFactory . Nous pouvons également utiliser d'autres méthodes comme celle donnée pour augmenter la lisibilité.

Les paramètres de synchronisation par défaut peuvent être modifiés à l'aide de méthodes statiques de la classe Awaitility :

Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS); Awaitility.setDefaultPollDelay(Duration.ZERO); Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);

Ici, nous pouvons voir l'utilisation de la classe Duration , qui fournit des constantes utiles pour les périodes de temps les plus fréquemment utilisées.

Nous pouvons également fournir des valeurs de synchronisation personnalisées pour chaque appel en attente . Ici, nous prévoyons que l'initialisation se produira au plus après cinq secondes et au moins après 100 ms avec des intervalles d'interrogation de 100 ms:

asyncService.initialize(); await() .atLeast(Duration.ONE_HUNDRED_MILLISECONDS) .atMost(Duration.FIVE_SECONDS) .with() .pollInterval(Duration.ONE_HUNDRED_MILLISECONDS) .until(asyncService::isInitialized);

Il convient de mentionner que le ConditionFactory contient des méthodes supplémentaires comme avec , puis , et , étant donné. Ces méthodes ne font rien et renvoient simplement ceci , mais elles pourraient être utiles pour améliorer la lisibilité des conditions de test.

5. Utilisation de Matchers

Awaitility permet également d'utiliser des matchers hamcrest pour vérifier le résultat d'une expression. Par exemple, nous pouvons vérifier que notre valeur longue est modifiée comme prévu après avoir appelé la méthode addValue :

asyncService.initialize(); await() .until(asyncService::isInitialized); long value = 5; asyncService.addValue(value); await() .until(asyncService::getValue, equalTo(value));

Notez que dans cet exemple, nous avons utilisé la première await attendre jusqu'à ce que le service est initialisé appel. Sinon, la méthode getValue lèverait une IllegalStateException .

6. Ignorer les exceptions

Parfois, nous avons une situation où une méthode lève une exception avant qu'un travail asynchrone ne soit effectué. Dans notre service, il peut s'agir d'un appel à la méthode getValue avant l'initialisation du service.

L'attente offre la possibilité d'ignorer cette exception sans échouer à un test.

Par exemple, vérifions que le résultat getValue est égal à zéro juste après l'initialisation, en ignorant IllegalStateException :

asyncService.initialize(); given().ignoreException(IllegalStateException.class) .await().atMost(Duration.FIVE_SECONDS) .atLeast(Duration.FIVE_HUNDRED_MILLISECONDS) .until(asyncService::getValue, equalTo(0L));

7. Utilisation du proxy

Comme décrit dans la section 2, nous devons inclure le proxy d' attente pour utiliser des conditions basées sur un proxy. L'idée du proxy est de fournir de vrais appels de méthode pour des conditions sans implémentation d'une expression Callable ou lambda.

Utilisons la AwaitilityClassProxy.to méthode statique pour vérifier que AsyncService est initialisé:

asyncService.initialize(); await() .untilCall(to(asyncService).isInitialized(), equalTo(true));

8. Accès aux champs

Awaitility peut même accéder à des champs privés pour y effectuer des assertions. Dans l'exemple suivant, nous pouvons voir une autre façon d'obtenir l'état d'initialisation de notre service:

asyncService.initialize(); await() .until(fieldIn(asyncService) .ofType(boolean.class) .andWithName("initialized"), equalTo(true));

9. Conclusion

Dans ce rapide tutoriel, nous avons présenté la bibliothèque Awaitility, nous nous sommes familiarisés avec son DSL de base pour le test des systèmes asynchrones et avons vu quelques fonctionnalités avancées qui rendent la bibliothèque flexible et facile à utiliser dans de vrais projets.

Comme toujours, tous les exemples de code sont disponibles sur Github.