Introduction à l’attente

1. Introduction

Un problème commun aux systèmes asynchrones est qu’il est difficile d’écrire 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’exécution et le contrôle des accès concurrents.

Dans cet article, nous allons jeter un oeil à Awaitility - une bibliothèque qui fournit un langage DSL simple pour des tests de systèmes asynchrones .

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

2. Dépendances

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

La bibliothèque awaitility sera suffisante pour la plupart des cas d’utilisation. Si nous voulons utiliser les conditions basées sur le proxy _, , nous devons également fournir la bibliothèque awaitility-proxy_ :

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility-proxy</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>

Vous pouvez trouver la dernière version de https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.awaitility%22%20AND%20a%3A%22awaitility%22&amp ; awaitility ]et waitility-proxy bibliothèques 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. Tester avec l’attente

Créons maintenant la classe de test:

public class AsyncServiceTest {
    private AsyncService asyncService;

    @Before
    public void setUp() {
        asyncService = new AsyncService();
    }

   //...
}

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

Ce scénario de test attend simplement que l’état d’initialisation du service soit modifié ou lève une ConditionTimeoutException si le changement d’état ne se produit pas.

L’état est obtenu par un Callable 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). Nous utilisons ici les paramètres par défaut pour le délai d’attente, l’intervalle et le délai:

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

Ici, nous utilisons await - une des méthodes statiques de la classe Awaitility . Il retourne une instance d’une classe ConditionFactory . Nous pouvons également utiliser d’autres méthodes comme given pour améliorer 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);

Nous pouvons voir ici l’utilisation de la classe Duration , qui fournit des constantes utiles pour les périodes les plus fréquemment utilisées.

Nous pouvons également fournir des valeurs de synchronisation personnalisées pour chaque appel await . Nous nous attendons ici à ce que l’initialisation se produise au plus tard 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 est à noter que la ConditionFactory contient des méthodes supplémentaires telles que with , then , and , given. Ces méthodes ne font rien et renvoient simplement cette , mais elles pourraient être utiles pour améliorer la lisibilité des conditions de test.

5. Utiliser des correspondants

Awaitility permet également l’utilisation de hamcrest matchers pour vérifier le résultat d’une expression. Par exemple, nous pouvons vérifier que notre valeur long est modifiée comme prévu après l’appel de 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é le premier appel await pour attendre que le service soit initialisé. Sinon, la méthode getValue lèverait une IllegalStateException .

6. Ignorer les exceptions

Il arrive parfois qu’une méthode lève une exception avant qu’un travail asynchrone soit effectué. Dans notre service, il peut s’agir d’un appel à la méthode getValue avant l’initialisation du service.

Awaitility 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. Utiliser un proxy

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

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

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

8. Accéder aux champs

Awaitility peut même accéder à des champs privés pour y effectuer des assertions.

Dans l’exemple suivant, nous pouvons voir un autre moyen 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 didacticiel, nous avons présenté la bibliothèque Awaitility, familiarisé avec son ADSL de base pour le test de systèmes asynchrones et vu quelques fonctionnalités avancées qui rendent la bibliothèque flexible et facile à utiliser dans des projets réels.

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