java - test - junit tutorial
Cómo usar Junit para probar procesos asíncronos (16)
¿Cómo prueba los métodos que desencadenan procesos asincrónicos con Junit?
No sé cómo hacer para que mi prueba espere a que termine el proceso (no es exactamente una prueba unitaria, es más como una prueba de integración ya que involucra varias clases y no solo una)
¿Qué tal si SomeObject.wait
y notifyAll
como se describe here O usando el método Solo.waitForCondition(...)
O utilizamos una clase que escribí para hacer esto (ver los comentarios y la clase de prueba para saber cómo usarlo)
Aquí hay muchas respuestas, pero una simple es simplemente crear un CompletableFuture completado y usarlo:
CompletableFuture.completedFuture("donzo")
Entonces en mi prueba:
this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));
Solo me aseguro de que todas estas cosas se llamen de todos modos. Esta técnica funciona si estás usando este código:
CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();
Se cerrará a medida que finalicen todos los Fines Completables.
Comience el proceso y espere el resultado usando un Future
.
En mi humilde opinión, es una mala práctica tener pruebas unitarias para crear o esperar en hilos, etc. Desea que estas pruebas se ejecuten en fracciones de segundo. Es por eso que me gustaría proponer un enfoque en dos pasos para probar procesos asincrónicos.
- Compruebe que su proceso asíncrono se envía correctamente. Puede simular el objeto que acepta sus solicitudes de sincronización y asegurarse de que el trabajo enviado tenga las propiedades correctas, etc.
- Prueba que tus devoluciones de llamadas asincrónicas están haciendo lo correcto. Aquí puede simular el trabajo enviado originalmente y asumir que se inicializó correctamente y verificar que sus devoluciones de llamada sean correctas.
Encuentro una biblioteca socket.io para probar la lógica asíncrona. Se ve una forma simple y breve usando LinkedBlockingQueue . Aquí hay un example :
@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 API para bloquear hasta obtener el resultado de la misma manera sincrónica. Y configure el tiempo de espera para evitar asumir demasiado tiempo para esperar el resultado.
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 un poco agradable. (nota, en este ejemplo estoy comenzando 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 f.complete
. También puede usar f.completeExceptionally
para fallar temprano.
Evite las pruebas con hilos paralelos siempre que pueda (lo cual es la mayoría de las veces). Esto solo hará que tus pruebas sean escamosas (a veces pasan, a veces fallan).
Solo cuando necesite llamar a otra biblioteca / sistema, es posible que deba esperar otros hilos, en ese caso siempre use la biblioteca Awaitility lugar de Thread.sleep()
.
Nunca solo llame a get()
o join()
en sus pruebas, de lo contrario sus pruebas podrían ejecutarse para siempre en su servidor de CI en caso de que el futuro nunca se complete. Siempre afirme isDone()
primero en sus pruebas antes de llamar a get()
. Para CompletionStage, es .toCompletableFuture().isDone()
.
Cuando prueba 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 debería simplemente probar el resultado pasando un Future completado en la prueba, también debería asegurarse de que su método doSomething()
no se bloquee llamando a join()
o get()
. Esto es importante en particular si usa un marco sin bloqueo.
Para hacer eso, pruebe con un futuro no completado que configure para que se complete 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 esta forma, si agrega future.join()
a doSomething (), la prueba fallará.
Si su Servicio utiliza un ExecutorService como en thenApplyAsync(..., executorService)
, en sus pruebas inyecte un EjecutorService de un solo subproceso, como el de guava:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Si su código usa el forkJoinPool como, por ejemplo, thenApplyAsync(...)
, thenApplyAsync(...)
escribir el código para usar un ExecutorService (hay muchas buenas razones), o use Awaitility.
Para acortar el ejemplo, hice BarService un argumento de método implementado como lambda Java8 en la prueba, típicamente sería una referencia inyectada de la que se burlaría.
No hay nada intrínsecamente incorrecto en la prueba del código roscado / asíncrono, especialmente si el enhebrado es el punto del código que está probando. El enfoque general para probar esto es:
- Bloquear el hilo de prueba principal
- Capturar afirmaciones fallidas de otros hilos
- Desbloquear el hilo de prueba principal
- Retira cualquier falla
Pero eso es un montón de repetición 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 de CountdownLatch
es que es menos detallado porque las fallas de afirmación que ocurren en cualquier hilo se informan adecuadamente al hilo principal, lo que significa que la prueba falla cuando debería. Se encuentra aquí una reseña que compara el enfoque CountdownLatch
con ConcurrentUnit.
También escribí una here sobre el tema para aquellos que quieren aprender un poco más de detalle.
Para aquellos que les gusta aprender con el ejemplo, creo que este es uno bueno: https://github.com/playframework/play-java-ebean-example/blob/2.6.x/test/ModelTest.java
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 de matriz final, para ser utilizada dentro de la clase interna anónima. Prefiero crear un booleano [], porque puedo poner un valor para controlar si tenemos que esperar (). Cuando todo está hecho, simplemente lanzamos el asyncExecuted.
Puedes intentar usar la biblioteca Awaitility . Hace que sea fácil probar los sistemas de los que está hablando.
Si desea probar la lógica, simplemente no la pruebe de forma asíncrona.
Por ejemplo, para probar este código que funciona en 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 la implementación sincrónica. 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 asíncrono, pero puede probar si la lógica es correcta.
Si usa un CompletableFuture (introducido en Java 8) o un SettableFuture (de Google Guava ), puede hacer que la prueba termine tan pronto como se termine, en lugar de esperar una cantidad de tiempo preestablecida. Su prueba se vería así:
CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {
@Override
public void run() {
future.complete("Hello World!");
}
});
assertEquals("Hello World!", future.get());
Un método que he encontrado bastante útil para probar métodos asíncronos es inyectar una instancia Executor
en el constructor de objeto a prueba. En producción, la instancia del ejecutor está configurada para ejecutarse de forma asíncrona, mientras que en la prueba se puede simular que se ejecuta de forma síncrona.
Entonces, 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 de Executors.newSingleThreadExecutor()
mientras que en la prueba probablemente lo construiría con un ejecutor síncrono que hace lo siguiente:
class SynchronousExecutor implements Executor {
@Override public void execute(Runnable r) {
r.run();
}
}
Ahora mi prueba JUnit del método asincrónico 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());
}
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 se puede usar sincronizado con un objeto normal 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 this publicación del blog por Joe Walnes.
EDITAR Eliminó bloques sincronizados alrededor de CountDownLatch gracias a los comentarios de @jtahlborn y @Ring
Vale la pena mencionar que hay un capítulo muy útil Testing Concurrent Programs
concurrentes en simultaneidad en la práctica que describe algunos enfoques de pruebas de unidades y brinda soluciones a problemas.