Comment tester unitairement du code asynchrone en Java

Comment tester unitairement du code asynchrone en Java

Je m’essaye à quelque chose d’un peu différent de ce que j’écris habituellement : la présentation d’un outil qui m’a été utile. N’hésitez pas à me dire si ce type d’article vous intéresse également !

Tous ceux qui ont eu le plaisir de tester unitairement des opérations asynchrone en java savent que cela se transforme souvent en cauchemars.

Le problème est de réussir à cadencer correctement les différentes parties du code, et attendre que les opérations asynchrones aient bien été réalisées pour pouvoir lancer les assertions.

C’est souvent l’occasion de voir des bricolages très inventifs, souvent à l’aide de callbacks, flags et autres Thread.sleep(). Avec le risque d’avoir des tests fragiles qui se retrouvent bloqués dans leur exécution.

	var result = somethingAsync();
    
	Thread.sleep(100);
    
	assertThat(result).isEqualTo(...);
Un exemple à base de Thread.sleep
	io.vertx.core.Future<?> future = somethingAsync();
    
	while(!future.isComplete()) {
		Thread.sleep(10);
	}
    
	assertThat(future.result()).isEqualTo(...);
Utilisation d'un while true pour attendre l'achèvement d'un future vertx

Awaitility à la rescousse

En cherchant comment améliorer le test unitaire d’une fonction asynchrone, je suis tombé sur un outil très sympathique dédié à ce cas d’usage : awaitility.

Grâce à lui nous pouvons réécrire le test précédent avec de manière beaucoup plus agréable :

var future = somethingAsync();

await().pollInterval(10, TimeUnit.MILLISECONDS) //try every 10ms
       .atMost(50, TimeUnit.MILLISECONDS)       //timeout after 50ms
       .until(future::isComplete);
       
assertThat(future.result()).isEqualTo(...);
Utilisation d'awaitility pour attendre l'achèvement d'un future vertx

Le DSL est très simple mais suffisamment bien pensé pour pouvoir s’adapter à plein de situations. Par exemple, si vous avez besoin d’attendre un effet de bord asynchrone.

await().pollDelay(10, TimeUnit.MILLISECONDS) //wait 10ms before first try
       .atMost(50, TimeUnit.MILLISECONDS)    //timeout after 50ms
       .until(() -> persistence.exists("someId"));
Utilisation d'awaitility pour vérifier qu'une opération a bien été réalisée avant de continuer l'exécution

Pour être tenu au courant des prochains articles ou conférences, vous pouvez les recevoir par mail. Et si vous avez apprécié cet article, c'est l'occasion de le partager sur twitter, Linkedin, ou sur Slack.