web-dev-qa-db-fra.com

Comment utiliser JUnit pour tester des processus asynchrones

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).

177
Sam

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.

  1. Vérifiez que votre processus asynchrone est correctement soumis. Vous pouvez simuler l'objet qui accepte vos demandes asynchrones et vous assurer que le travail soumis a les propriétés correctes, etc.
  2. Vérifiez que vos rappels asynchrones agissent correctement. Ici, vous pouvez simuler le travail soumis à l'origine et supposer qu'il est correctement initialisé et vérifier que vos rappels sont corrects.
43
Cem Catikkas

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

179
Martin

Vous pouvez essayer d'utiliser la bibliothèque Awaitility . Il est facile de tester les systèmes dont vous parlez.

65
Johan

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());
60
user393274

Démarrez le processus et attendez le résultat à l'aide de Future .

22

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());
}
17
Matt

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 à:

  • Bloquer le fil de test principal
  • Capturer les assertions ayant échoué à partir d'autres threads
  • Débloquer le fil de test principal
  • Renouveler les échecs

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.

5
Jonathan

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)

4
Dori

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.

3
eleven

É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.

2
tkruse

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!

2
markthegrea

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.

1
Fantasy Fang

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.

1
Paulo

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.

1
Jochen Bedersdorfer

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.

0
Nils El-Himoud