Cómo usar Junit para probar procesos asíncronos


¿Cómo se prueban los métodos que activan procesos asíncronos con Junit?

No se cómo hacer que mi prueba espere a que el proceso termine (no es exactamente una prueba unitaria, es más como una prueba de integración, ya que involucra varias clases y no solo una)

Author: Sam, 2009-03-10

15 answers

En mi humilde opinión es una mala práctica que las pruebas unitarias creen o esperen en subprocesos, etc. Te gustaría que estas pruebas se ejecutaran en fracciones de segundo. Es por eso que me gustaría proponer un enfoque de 2 pasos para probar procesos asincrónicos.

  1. Pruebe que su proceso asincrónico se envía correctamente. Puede simular el objeto que acepta sus solicitudes asincrónicas y asegurarse de que el trabajo enviado tiene las propiedades correctas, etc.
  2. Prueba que tus callbacks async están haciendo las cosas correctas. Aquí usted puede burlarse de la trabajo enviado originalmente y asumir que se inicializa correctamente y verificar que sus devoluciones de llamada son correctas.
 40
Author: Cem Catikkas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-10 18:53:30

Una alternativa es usar la clase 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());
    }
}

NOTA no puedes simplemente usar sincronizado con un objeto regular como un bloqueo, ya que las devoluciones de llamada rápidas pueden liberar el bloqueo antes de que se llame al método de espera del bloqueo. Ver esta entrada de blog de Joe Walnes.

EDITAR Se han eliminado los bloques sincronizados alrededor de CountDownLatch gracias a los comentarios de @jtahlborn y @Ring

 152
Author: Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-05 20:35:08

Puede intentar usar la biblioteca Awaitility. Hace que sea fácil probar los sistemas de los que estás hablando.

 56
Author: Johan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-26 20:05:27

Si utiliza un CompletableFuture (introducido en Java 8) o un SettableFuture (desde Google Guava), puede terminar su prueba tan pronto como esté terminado, en lugar de esperar una cantidad de tiempo preestablecida. Su prueba se vería algo como esto:

CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {         
    @Override
    public void run() {
        future.complete("Hello World!");                
    }
});
assertEquals("Hello World!", future.get());
 40
Author: user393274,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-01 16:54:02

Iniciar el proceso apagado y esperar el resultado utilizando un Future.

 21
Author: Tom Hawtin - tackline,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-10 18:12:49

Un método que he encontrado bastante útil para probar métodos asíncronos es inyectar una instancia Executor en el constructor del objeto a prueba. En producción, la instancia ejecutora está configurada para ejecutarse de forma asíncrona, mientras que en prueba se puede burlar de ella para que se ejecute de forma sincrónica.

Así que supongamos que estoy tratando de probar el método asíncrono 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 producción, construiría Foo con una instancia Ejecutora Executors.newSingleThreadExecutor() mientras que en prueba probablemente la construiría con una instancia síncrona ejecutor que hace lo siguiente --

class SynchronousExecutor implements Executor {
  @Override public void execute(Runnable r) {
    r.run();
  }
}

Ahora mi prueba JUnit del método asíncrono es bastante limpia {

@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());
}
 15
Author: Matthew,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-13 19:31:57

¿Qué tal llamar a SomeObject.wait y notifyAll como se describe aquí O usar Robotiums Solo.waitForCondition(...) método O use una clase que escribí para hacer esto (ver comentarios y clase de prueba para saber cómo usar)

 4
Author: Dori,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-14 12:14:55

No hay nada inherentemente malo en probar código threaded/async, particularmente si threading es el punto del código que estás probando. El enfoque general para probar estas cosas es:

  • Bloquear el hilo de prueba principal
  • Captura aserciones fallidas de otros hilos
  • Desbloquear el hilo de prueba principal
  • Repensar cualquier fallo

Pero eso es un montón de repeticiones para una prueba. Un enfoque mejor / más simple es simplemente usar ConcurrentUnit :

  final Waiter waiter = new Waiter();

  new Thread(() -> {
    doSomeWork();
    waiter.assertTrue(true);
    waiter.resume();
  }).start();

  // Wait for resume() to be called
  waiter.await(1000);

El beneficio de esto sobre el enfoque CountdownLatch es que es menos detallado ya que los errores de aserción que ocurren en cualquier subproceso se informan correctamente al subproceso principal, lo que significa que la prueba falla cuando debería. Una valoración crítica que compara el enfoque CountdownLatch para ConcurrentUnit es aquí.

También escribí una entrada de blog sobre el tema para aquellos que quieren aprender un poco más de detalle.

 4
Author: Jonathan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-17 05:03:20

Prefiero usar esperar y notificar. Es simple y claro.

@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];
    }
}

Básicamente, necesitamos crear una referencia final de matriz, para ser utilizada dentro de la clase interna anónima. Prefiero crear un booleano [], porque puedo poner un valor para controlar si necesitamos wait(). Cuando todo está hecho, solo liberamos el asyncExecuted.

 2
Author: Paulo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-09 14:59:26

Vale la pena mencionar que hay un capítulo muy útil Testing Concurrent Programs en Concurrency in Practice que describe algunos enfoques de pruebas unitarias y da soluciones para los problemas.

 2
Author: eleven,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-23 13:39:24

Encuentro una biblioteca socket.io para probar la lógica asíncrona. Se ve de manera simple y breve usando LinkedBlockingQueue . Aquí está ejemplo :

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

Usando LinkedBlockingQueue tome la API para bloquear hasta obtener el resultado al igual que la forma síncrona. Y establecer tiempo de espera para evitar asumir demasiado tiempo para esperar el resultado.

 1
Author: Fantasy Fang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-17 02:55:48

Hay muchas respuestas aquí, pero una simple es simplemente crear un CompletableFuture completado y usarlo:

CompletableFuture.completedFuture("donzo")

Así que en mi prueba:

this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));

Solo me estoy asegurando de que todas estas cosas se llaman de todos modos. Esta técnica funciona si está utilizando este código:

CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();

Se comprimirá a través de él como todos los CompletableFutures están terminados!

 1
Author: markthegrea,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-26 20:14:37

Evite probar con subprocesos paralelos siempre que pueda (que es la mayor parte del tiempo). Esto solo hará que sus pruebas sean escamosas (a veces pasan, a veces fallan).

Solo cuando necesite llamar a otra biblioteca / sistema, es posible que tenga que esperar en otros hilos, en ese caso siempre use la biblioteca Awaitility en lugar de Thread.sleep().

Nunca simplemente llame a get() o join() en sus pruebas, de lo contrario, sus pruebas podrían ejecutarse para siempre en su servidor CI en caso de que el futuro nunca se complete. Siempre assert isDone() primero en tus pruebas antes de llamar a get(). Para CompletionStage, eso es .toCompletableFuture().isDone().

Cuando pruebas un método sin bloqueo como este:

public static CompletionStage<Foo> doSomething(BarService service) {
    CompletionStage<Bar> future = service.getBar();
    return future.thenApply(bar -> fooToBar());
}

Entonces no solo debe probar el resultado pasando un Futuro completado en la prueba, también debe asegurarse de que su método doSomething() no se bloquee llamando a join() o get(). Esto es importante, en particular, si se utiliza un marco sin bloqueo.

Para hacer eso, prueba con un futuro no completado que establezcas en completado manualmente:

@Test
public void testDoSomething() {
    CompletableFuture<Bar> innerFuture = new CompletableFuture<>();
    fooResult = doSomething(() -> innerFuture).toCompletableFuture();
    assertFalse(fooResult.isDone());

    // this triggers the future to complete
    innerFuture.complete(new Bar());
    assertTrue(fooResult.isDone());

    // futher asserts about fooResult here
}

De esa manera, si agregas future.join() a doSomething(), la prueba fallará.

Si su Servicio usa un ExecutorService como en thenApplyAsync(..., executorService), entonces en sus pruebas inyecte un ExecutorService de un solo subproceso, como el de guava:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Si su código usa el ForkJoinPool como thenApplyAsync(...), reescriba el código para usar un ExecutorService (hay muchas buenas razones), o use Awaitility.

Para acortar el ejemplo, hice BarService un método argumento implementado como un lambda Java8 en la prueba, por lo general sería una referencia inyectada que se burlaría.

 1
Author: tkruse,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-06 07:27:16

Si quieres probar la lógica, simplemente no la pruebes asincrónicamente.

Por ejemplo, para probar este código que funciona sobre los resultados de un método asíncrono.

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       
    }
}

En la prueba simula la dependencia con implementación síncrona. La prueba unitaria es completamente sincrónica y se ejecuta 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")));
    }
}

No prueba el comportamiento asincrónico, pero puede probar si la lógica es correcta.

 0
Author: Nils El-Himoud,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-29 08:18:29

Esto es lo que estoy usando hoy en día si el resultado de la prueba se produce de forma asíncrona.

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

Usando importaciones estáticas, la prueba se lee bastante bien. (nota, en este ejemplo estoy empezando un hilo para ilustrar la idea)

    @Test
    public void testAsync() {
        String result = await(f -> {
            new Thread(() -> f.complete("My Result")).start();
        });
        assertEquals("My Result", result);
    }

Si no se llama a f.complete, la prueba fallará después de un tiempo de espera. También puede usar f.completeExceptionally para fallar temprano.

 0
Author: Jochen Bedersdorfer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-29 01:35:38