Tests java asynchrone : AssertRetry
Par Francois Wauquier le dimanche 20 juin 2010, 14:02 - Lien permanent
Je souhaite ici partager une expérience lors de l'écriture de tests unitaires en Java
Le Besoin
Ecrire un test dont l'action est asynchrone (ex: batch, thread, workflow, ...)
L'Erreur
Ajouter un Thread.sleep() entre l'action et les assertions
Cela va temporairement résoudre votre problème : le test va passer.
Mais cela a de nombreuses contraintes :
- Votre série de tests va commencer à être très longue.
- À chaque fois que le test va échouer sur la machine d'un développeur, celui-ci va être tenté d'augmenter le délai d'attente.
Ainsi, la durée de votre test est contrainte par la machine la plus lente.
Personne ne veut avoir une suite de test longue, encore moins une suite qui ne fait qu’attendre.
Le timeout sur les tests apporté par les annotations Junit 4 permet de définir un temps d’exécution maximal pour l’ ensemble de la méthode de test, mais il ne permet pas de placer les assertions à la fin du timeout. Il nous faut donc trouver d’autres solutions.
Solution 0 : Contourner
Si cela est possible, placer les tests plus près du code, et faire un test simple.
Solution 1 : Listener
Réussir à observer la fin du traitement par une attente passive (Pattern observateur), associé à un timeout.
C'est ce qui est fait par exemple dans GwtTestCase
http://code.google.com/intl/fr/webtoolkit/doc/1.6/DevGuideTesting.html#DevGuideAsynchronousTesting
La durée d’exécution du test est ainsi parfaitement ajustée à la durée de l'action testée.
|
public void testDoWithCallback() throws Exception { final Async async = new Async(); async.doWithCallBack(new Async.Callback() { public void onSuccess() { finishTest(); } public void onError(Throwable t) { finishTest(t); } }); delayTestFinish(11000); assertEquals(10, async.result); } |
Il existe une variante avec wait et notify, mais le code est moins lisible (voir sources).
Solution 2 : AssertRetry
Dans le cas où vous ne pouvez pas être notifié de la fin du traitement, ce qui était mon cas.
La solution que j'ai trouvée est de faire plusieurs essais successifs afin de vérifier la fin correcte du traitement.
Les assertions sont définies avec un nombre d'essais supplémentaires et une durée d'attente entre les essais.
Ainsi, le test passera avec peu d’essais sur une machine puissante, et passera également sur une machine moins puissante,
jusqu’à atteindre le timeout implicite ( nombre d’essais supplémentaires * durée d’attente), dans ce cas, la dernière assertion échoue.
|
public void testDoWithoutCallback() throws Exception { final Async async = new Async(); async.doWithoutCallBack(); new AssertRetry(10,1000, new AssertRetry.AssertTry() { public void assertTry() throws Exception { assertEquals(10, async.result); } }); } |
Conclusion
J’ai trouvé cette solution du AssertRetry efficace et élégante.
Je n’ai pas trouvé d’équivalent dans les frameworks de test, mais si vous en connaissez un, faites-moi signe.
Le code fourni peut être consommé sur place ou à emporter.
|
package fr.wokier.assertion;
import org.apache.log4j.Logger;
/** * Allows to give multiple tries to some assertions, without adding arbitrary and * time consuming Thread.sleep() calls * * @author f_wauquier */ public class AssertRetry {
private static final Logger LOGGER = Logger.getLogger(AssertRetry.class);
/** * Internal class for AssertRetry * */ public interface AssertTry { /** * Does one or more assertions * * @throws Exception * eorror or assertion failure */ public void assertTry() throws Exception; }
/** * Builds an AssertRetry * * @param retryTimes * number of times the assertTry will be called AGAIN (0 calls it * only one time) * @param assertTry * Implements your own AssertTry containing assertions * @throws Exception */ public AssertRetry(int retryTimes, AssertTry assertTry) throws Exception { retry(retryTimes, 0, assertTry); }
/** * Builds an AssertRetry * * @param retryTimes * number of times the assertTry will be called AGAIN (0 calls it * only one time) * @param sleepTimeInMillis * sleep time between assertTry calls, in milliseconds * @param assertTry * Implements your own AssertTry containing assertions * @throws Exception */ public AssertRetry(int retryTimes, int sleepTimeInMillis, AssertTry assertTry) throws Exception { retry(retryTimes, sleepTimeInMillis, assertTry); }
private void retry(int retryTimes, int sleepTimeInMillis, AssertTry assertTry) throws Exception { if (retryTimes == 0) { assertTry.assertTry(); } else { try { assertTry.assertTry(); } catch (Throwable e) { LOGGER.warn("Try failed (remain " + retryTimes + ") :" + e.getMessage()); Thread.sleep(sleepTimeInMillis); retry(retryTimes - 1, sleepTimeInMillis, assertTry); } } } }
|
Sources
