Comment tester les méthodes qui déclenchent des processus asynchrones avec JUnit?
Je ne sais pas comment faire attendre mon processus jusqu'à la fin du processus (il ne s'agit pas exactement d'un test unitaire, mais d'un test d'intégration, car il implique plusieurs classes et non une seule).
IMHO c'est une mauvaise pratique d'avoir des tests unitaires créer ou attendre sur les threads, etc. Vous voudriez que ces tests soient exécutés en quelques secondes. C'est pourquoi j'aimerais proposer une approche en deux étapes pour tester les processus asynchrones.
Une alternative consiste à utiliser la classe CountDownLatch .
public class DatabaseTest {
/**
* Data limit
*/
private static final int DATA_LIMIT = 5;
/**
* Countdown latch
*/
private CountDownLatch lock = new CountDownLatch(1);
/**
* Received data
*/
private List<Data> receiveddata;
@Test
public void testDataRetrieval() throws Exception {
Database db = new MockDatabaseImpl();
db.getData(DATA_LIMIT, new DataCallback() {
@Override
public void onSuccess(List<Data> data) {
receiveddata = data;
lock.countDown();
}
});
lock.await(2000, TimeUnit.MILLISECONDS);
assertNotNull(receiveddata);
assertEquals(DATA_LIMIT, receiveddata.size());
}
}
[~ # ~] note [~ # ~] vous ne pouvez pas simplement utiliser syncronisé avec un objet standard comme verrou, car des rappels rapides peuvent libérer le verrou avant que la méthode d'attente du verrou ne soit appelée. Voir this blog de Joe Walnes.
[~ # ~] edit [~ # ~] Suppression des blocs synchronisés autour de CountDownLatch grâce aux commentaires de @jtahlborn et @Ring
Vous pouvez essayer d'utiliser la bibliothèque Awaitility . Il est facile de tester les systèmes dont vous parlez.
Si vous utilisez un CompletableFuture (introduit dans Java 8)) ou un SettableFuture (de Google Guava ), vous pouvez terminer votre test dès que vous avez terminé, au lieu d'attendre une -set quantité de temps. Votre test ressemblerait à ceci:
CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {
@Override
public void run() {
future.complete("Hello World!");
}
});
assertEquals("Hello World!", future.get());
Démarrez le processus et attendez le résultat à l'aide de Future
.
Une méthode que j'ai trouvée assez utile pour tester des méthodes asynchrones consiste à injecter une instance Executor
dans le constructeur de l'objet à tester. En production, l'instance de l'exécuteur est configurée pour s'exécuter de manière asynchrone, alors qu'elle est en cours de test, elle peut être simulée pour s'exécuter de manière synchrone.
Alors supposons que je suis en train de tester la méthode asynchrone Foo#doAsync(Callback c)
,
class Foo {
private final Executor executor;
public Foo(Executor executor) {
this.executor = executor;
}
public void doAsync(Callback c) {
executor.execute(new Runnable() {
@Override public void run() {
// Do stuff here
c.onComplete(data);
}
});
}
}
En production, je construirais Foo
avec une instance de Executors.newSingleThreadExecutor()
Executor, tandis que dans le test, je le construirais probablement avec un exécuteur synchrone qui effectue les opérations suivantes -
class SynchronousExecutor implements Executor {
@Override public void execute(Runnable r) {
r.run();
}
}
Maintenant, mon test JUnit de la méthode asynchrone est assez propre -
@Test public void testDoAsync() {
Executor executor = new SynchronousExecutor();
Foo objectToTest = new Foo(executor);
Callback callback = mock(Callback.class);
objectToTest.doAsync(callback);
// Verify that Callback#onComplete was called using Mockito.
verify(callback).onComplete(any(Data.class));
// Assert that we got back the data that we expected.
assertEquals(expectedData, callback.getData());
}
Il n'y a rien de mal à tester le code threaded/async, en particulier si le threading est le point du code que vous testez. L’approche générale pour tester ce matériel consiste à:
Mais c'est beaucoup de passe-partout pour un test. Une approche meilleure/plus simple consiste simplement à utiliser ConcurrentUnit :
final Waiter waiter = new Waiter();
new Thread(() -> {
doSomeWork();
waiter.assertTrue(true);
waiter.resume();
}).start();
// Wait for resume() to be called
waiter.await(1000);
L'avantage de cette approche par rapport à l'approche CountdownLatch
est qu'elle est moins détaillée puisque les échecs d'assertion survenant dans un thread sont correctement signalés au thread principal, ce qui signifie que le test échoue quand il le devrait. Un article qui compare l'approche CountdownLatch
de ConcurrentUnit est ici .
J'ai aussi écrit un article de blog sur le sujet pour ceux qui veulent apprendre un peu plus en détail.
Pourquoi ne pas appeler SomeObject.wait
Et notifyAll
comme décrit ici OR en utilisant Robotiums Solo.waitForCondition(...)
méthode OR utilise un classe i a écrit pour ce faire (voir les commentaires et la classe de test pour savoir comment utiliser)
Il est à noter qu'il existe un chapitre très utile Testing Concurrent Programs
in Concurrence en pratique qui décrit certaines approches de test unitaire et fournit des solutions aux problèmes.
Évitez de tester avec des threads parallèles chaque fois que vous le pouvez (ce qui est la plupart du temps). Cela ne fera que rendre vos tests floconneux (parfois réussi, parfois échoué).
Lorsque vous devez appeler une autre bibliothèque/système, vous devrez peut-être attendre d'autres threads. Dans ce cas, utilisez toujours la bibliothèque Awaitility au lieu de Thread.sleep()
.
N'appelez jamais simplement get()
ou join()
dans vos tests, sinon vos tests pourraient s'exécuter indéfiniment sur votre serveur d'infrastructure si le futur ne se termine jamais. Toujours associer isDone()
en premier dans vos tests avant d'appeler get()
. Pour CompletionStage, il s'agit de .toCompletableFuture().isDone()
.
Lorsque vous testez une méthode non bloquante comme celle-ci:
public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
return future.thenApply(result -> "Hello " + result);
}
alors vous ne devriez pas simplement tester le résultat en passant un futur complet dans le test, vous devez également vous assurer que votre méthode doSomething()
ne bloque pas en appelant join()
ou get()
. Ceci est particulièrement important si vous utilisez un framework non bloquant.
Pour ce faire, testez avec un futur non terminé que vous avez défini comme terminé manuellement:
@Test
public void testDoSomething() throws Exception {
CompletableFuture<String> innerFuture = new CompletableFuture<>();
CompletableFuture<String> futureResult = createGreeting(innerFuture).toCompletableFuture();
assertFalse(futureResult.isDone());
// this triggers the future to complete
innerFuture.complete("world");
assertTrue(futureResult.isDone());
// futher asserts about fooResult here
assertEquals(futureResult.get(), "Hello world");
}
Ainsi, si vous ajoutez future.join()
à quelque chose (), le test échouera.
Si votre service utilise un ExecutorService tel que dans thenApplyAsync(..., executorService)
, alors dans vos tests, injectez un ExecutorService à un seul thread, tel que celui de guava:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Si votre code utilise le forkJoinPool tel que thenApplyAsync(...)
, réécrivez le code pour utiliser un service d'exécution (il existe de nombreuses bonnes raisons) ou utilisez Awaitility.
Pour raccourcir cet exemple, j'ai fait de BarService un argument de méthode implémenté sous la forme d'un lambda Java8 dans le test. Il s'agit généralement d'une référence injectée à simuler.
Il y a beaucoup de réponses ici, mais une simple consiste à créer un CompletableFuture terminé et à l'utiliser:
CompletableFuture.completedFuture("donzo")
Donc dans mon test:
this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));
Je m'assure simplement que tout cela est appelé de toute façon. Cette technique fonctionne si vous utilisez ce code:
CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();
Il sera zippé au fur et à mesure que tous les CompletableFutures sont terminés!
Je trouve une bibliothèque socket.io pour tester la logique asynchrone. Il semble simple et bref en utilisant LinkedBlockingQueue . Voici exemple :
@Test(timeout = TIMEOUT)
public void message() throws URISyntaxException, InterruptedException {
final BlockingQueue<Object> values = new LinkedBlockingQueue<Object>();
socket = client();
socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
@Override
public void call(Object... objects) {
socket.send("foo", "bar");
}
}).on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
@Override
public void call(Object... args) {
values.offer(args);
}
});
socket.connect();
assertThat((Object[])values.take(), is(new Object[] {"hello client"}));
assertThat((Object[])values.take(), is(new Object[] {"foo", "bar"}));
socket.disconnect();
}
En utilisant LinkedBlockingQueue, prenez l’API pour bloquer jusqu’à obtenir le résultat de la même manière que la méthode synchrone. Et définissez le délai d’attente pour éviter de prendre trop de temps pour attendre le résultat.
Je préfère utiliser attendre et notifier. C'est simple et clair.
@Test
public void test() throws Throwable {
final boolean[] asyncExecuted = {false};
final Throwable[] asyncThrowable= {null};
// do anything async
new Thread(new Runnable() {
@Override
public void run() {
try {
// Put your test here.
fail();
}
// lets inform the test thread that there is an error.
catch (Throwable throwable){
asyncThrowable[0] = throwable;
}
// ensure to release asyncExecuted in case of error.
finally {
synchronized (asyncExecuted){
asyncExecuted[0] = true;
asyncExecuted.notify();
}
}
}
}).start();
// Waiting for the test is complete
synchronized (asyncExecuted){
while(!asyncExecuted[0]){
asyncExecuted.wait();
}
}
// get any async error, including exceptions and assertationErrors
if(asyncThrowable[0] != null){
throw asyncThrowable[0];
}
}
Fondamentalement, nous devons créer une référence de tableau finale, à utiliser dans une classe interne anonyme. Je préférerais créer un booléen [], car je peux mettre une valeur à contrôler si nous devons attendre (). Lorsque tout est terminé, nous publions simplement asyncExecuted.
C’est ce que j’utilise actuellement si le résultat du test est généré de manière asynchrone.
public class TestUtil {
public static <R> R await(Consumer<CompletableFuture<R>> completer) {
return await(20, TimeUnit.SECONDS, completer);
}
public static <R> R await(int time, TimeUnit unit, Consumer<CompletableFuture<R>> completer) {
CompletableFuture<R> f = new CompletableFuture<>();
completer.accept(f);
try {
return f.get(time, unit);
} catch (InterruptedException | TimeoutException e) {
throw new RuntimeException("Future timed out", e);
} catch (ExecutionException e) {
throw new RuntimeException("Future failed", e.getCause());
}
}
}
En utilisant les importations statiques, le test lit un peu Nice. (note, dans cet exemple je commence un fil pour illustrer l'idée)
@Test
public void testAsync() {
String result = await(f -> {
new Thread(() -> f.complete("My Result")).start();
});
assertEquals("My Result", result);
}
Si f.complete
_ n'est pas appelé, le test échouera après un délai d'attente. Vous pouvez aussi utiliser f.completeExceptionally
échouer tôt.
Si vous voulez tester la logique, ne la testez pas de manière asynchrone.
Par exemple pour tester ce code qui fonctionne sur les résultats d'une méthode asynchrone.
public class Example {
private Dependency dependency;
public Example(Dependency dependency) {
this.dependency = dependency;
}
public CompletableFuture<String> someAsyncMethod(){
return dependency.asyncMethod()
.handle((r,ex) -> {
if(ex != null) {
return "got exception";
} else {
return r.toString();
}
});
}
}
public class Dependency {
public CompletableFuture<Integer> asyncMethod() {
// do some async stuff
}
}
Dans le test, simulez la dépendance avec une implémentation synchrone. Le test unitaire est complètement synchrone et s'exécute en 150 ms.
public class DependencyTest {
private Example sut;
private Dependency dependency;
public void setup() {
dependency = Mockito.mock(Dependency.class);;
sut = new Example(dependency);
}
@Test public void success() throws InterruptedException, ExecutionException {
when(dependency.asyncMethod()).thenReturn(CompletableFuture.completedFuture(5));
// When
CompletableFuture<String> result = sut.someAsyncMethod();
// Then
assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
String value = result.get();
assertThat(value, is(equalTo("5")));
}
@Test public void failed() throws InterruptedException, ExecutionException {
// Given
CompletableFuture<Integer> c = new CompletableFuture<Integer>();
c.completeExceptionally(new RuntimeException("failed"));
when(dependency.asyncMethod()).thenReturn(c);
// When
CompletableFuture<String> result = sut.someAsyncMethod();
// Then
assertThat(result.isCompletedExceptionally(), is(equalTo(false)));
String value = result.get();
assertThat(value, is(equalTo("got exception")));
}
}
Vous ne testez pas le comportement asynchrone, mais vous pouvez vérifier si la logique est correcte.