Как использовать Junit для тестирования асинхронных процессов
Как вы тестируете методы, которые запускают асинхронные процессы с помощью Junit?
Я не знаю, как заставить мой тест ждать завершения процесса (это не совсем как unit test, это больше похоже на тест интеграции, поскольку он включает несколько классов, а не только один)
Ответы
Ответ 1
ИМХО, это плохая практика, чтобы модульные тесты создавали или ждали потоки и т.д. Тебе нужны эти тесты в считанные секунды. Поэтому я хотел бы предложить двухэтапный подход к тестированию асинхронных процессов.
- Проверьте, что ваш асинхронный процесс отправлен правильно. Вы можете высмеять объект, который принимает ваши асинхронные запросы, и убедитесь, что отправленное задание имеет правильные свойства и т.д.
- Проверьте, что ваши асинхронные обратные вызовы делают правильные вещи. Здесь вы можете высмеять первоначально поданное задание и предположительно инициализировать его правильно и убедиться, что ваши обратные вызовы верны.
Ответ 2
Альтернативой является использование класса 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());
}
}
ПРИМЕЧАНИЕ вы не можете просто использовать syncronized с обычным объектом в качестве блокировки, так как быстрые обратные вызовы могут освободить блокировку до вызова метода ожидания блокировки. См. это сообщение блога Джо Уолнесом.
EDIT Удалены синхронизированные блоки вокруг CountDownLatch благодаря комментариям @jtahlborn и @Ring
Ответ 3
Вы можете попробовать использовать библиотеку Awaitility. Это позволяет легко тестировать системы, о которых вы говорите.
Ответ 4
Если вы используете CompletableFuture (представленный на Java 8) или SettableFuture (из Google Guava), вы можете завершить свой тест, как только это будет сделано, а не ждать предварительно установленное количество времени. Ваш тест будет выглядеть примерно так:
CompletableFuture<String> future = new CompletableFuture<>();
executorService.submit(new Runnable() {
@Override
public void run() {
future.complete("Hello World!");
}
});
assertEquals("Hello World!", future.get());
Ответ 5
Запустите процесс и подождите результат с помощью Future
.
Ответ 6
Один из методов, который я нашел довольно полезным для тестирования асинхронных методов, - это инъекция экземпляра Executor
в конструкторе объекта-теста. В процессе производства экземпляр-исполнитель настроен на асинхронное выполнение, в то время как при тестировании его можно издеваться, чтобы он выполнял синхронно.
Итак, предположим, что я пытаюсь проверить асинхронный метод 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);
}
});
}
}
В процессе создания я бы построил Foo
с экземпляром Executors.newSingleThreadExecutor()
Executor, тогда как в тесте я бы, вероятно, построил его с синхронным исполнителем, который делает следующее -
class SynchronousExecutor implements Executor {
@Override public void execute(Runnable r) {
r.run();
}
}
Теперь мой JUnit-тест асинхронного метода довольно чистый -
@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());
}
Ответ 7
Как насчет вызова SomeObject.wait
и notifyAll
, как описано здесь ИЛИ используя Robotiums Solo.waitForCondition(...)
метод OR используйте класс, который я написал, чтобы сделать это (см. комментарии и тестовый класс для использования)
Ответ 8
Там нет ничего неправильного в тестировании threaded/async-кода, особенно если потоковая передача является точкой кода, который вы тестируете. Общий подход к тестированию этого материала заключается в следующем:
- Заблокировать основной тестовый поток
- Захват неудачных утверждений из других потоков
- Разблокировать основной тестовый поток
- Повторить любые сбои
Но это много шаблонов для одного теста. Лучший/более простой подход - просто использовать ConcurrentUnit:
final Waiter waiter = new Waiter();
new Thread(() -> {
doSomeWork();
waiter.assertTrue(true);
waiter.resume();
}).start();
// Wait for resume() to be called
waiter.await(1000);
Преимущество этого подхода CountdownLatch
заключается в том, что он менее подробен, поскольку ошибки утверждения, возникающие в любом потоке, сообщаются в основной поток, что означает, что тест терпит неудачу, когда это необходимо. Запись, сравнивающая подход CountdownLatch
к ConcurrentUnit, здесь.
Я также написал сообщение в блоге по теме для тех, кто хочет узнать немного более подробно.
Ответ 9
Здесь много ответов, но простой - просто создать заполненный CompletableFuture и использовать его:
CompletableFuture.completedFuture("donzo")
Итак, в моем тесте:
this.exactly(2).of(mockEventHubClientWrapper).sendASync(with(any(LinkedList.class)));
this.will(returnValue(new CompletableFuture<>().completedFuture("donzo")));
Я просто убеждаюсь, что все это вызвано. Этот метод работает, если вы используете этот код:
CompletableFuture.allOf(calls.toArray(new CompletableFuture[0])).join();
Он будет прокручиваться через него, поскольку все завершающие завершающие функции завершены!
Ответ 10
Стоит отметить, что в Concurrency в Практике есть очень полезная глава Testing Concurrent Programs
, которая описывает некоторые подходы к единичному тестированию и дает решения для проблем.
Ответ 11
Я предпочитаю использовать wait и notify. Это просто и понятно.
@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];
}
}
В принципе, нам нужно создать окончательную ссылку на Array, которая будет использоваться внутри анонимного внутреннего класса. Я бы предпочел создать boolean [], потому что я могу установить значение для контроля, если нам нужно ждать(). Когда все будет сделано, мы просто освободим asyncExecuted.
Ответ 12
Это то, что я использую сейчас, если результат теста создается асинхронно.
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());
}
}
}
Используя статический импорт, тест читает добрый.
(обратите внимание: в этом примере я начинаю поток, чтобы проиллюстрировать идею)
@Test
public void testAsync() {
String result = await(f -> {
new Thread(() -> f.complete("My Result")).start();
});
assertEquals("My Result", result);
}
Если f.complete
не вызывается, тест завершится с ошибкой после таймаута. Вы также можете использовать f.completeExceptionally
для отказа раньше.
Ответ 13
Я обнаружил библиотеку socket.io для проверки асинхронной логики. Это выглядит просто и кратко, используя LinkedBlockingQueue. Вот пример:
@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();
}
Используя LinkedBlockingQueue, используйте API для блокировки до получения результата так же, как синхронно. И установите тайм-аут, чтобы избежать слишком много времени, чтобы ждать результата.
Ответ 14
Избегайте тестирования с параллельными потоками, когда вы можете (что происходит в большинстве случаев). Это только сделает ваши тесты нестабильными (иногда проходит, иногда не проходит).
Только когда вам нужно вызвать какую-то другую библиотеку/систему, вам, возможно, придется ждать в других потоках, в этом случае всегда используйте библиотеку Awaitility вместо Thread.sleep()
.
Никогда не вызывайте get()
или join()
в своих тестах, иначе ваши тесты могут работать вечно на вашем CI-сервере, если будущее никогда не завершится. Всегда проверяйте isDone()
сначала в своих тестах перед вызовом get()
. Для CompletionStage это .toCompletableFuture().isDone()
.
Когда вы тестируете неблокирующий метод, подобный этому:
public static CompletionStage<String> createGreeting(Supplier<CompletableFuture<String>> service) {
CompletionStage<String> future = service.get();
return future.thenApply(result -> "Hello " + result);
}
тогда вам не нужно просто проверять результат, передав в тесте завершенное Future, вы также должны убедиться, что ваш метод doSomething()
не блокируется, вызывая join()
или get()
. Это особенно важно, если вы используете неблокирующую среду.
Для этого выполните тестирование с незавершенным будущим, которое вы установили как завершенное вручную:
@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");
}
Таким образом, если вы добавите future.join()
в doSomething(), тест не пройден.
Если ваша служба использует ExecutorService, например, в thenApplyAsync(..., executorService)
, то в ваших тестах thenApplyAsync(..., executorService)
служба ExecutorService, например, из thenApplyAsync(..., executorService)
:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Если ваш код использует forkJoinPool, например thenApplyAsync(...)
, thenApplyAsync(...)
код для использования ExecutorService (есть много веских причин) или используйте Awaitility.
Чтобы сократить пример, я сделал BarService аргументом метода, реализованным как лямбда-код Java8 в тесте, обычно это будет внедренная ссылка, которую вы будете высмеивать.
Ответ 15
Если вы хотите протестировать логику, просто не проверяйте ее асинхронно.
Например, чтобы проверить этот код, который работает с результатами асинхронного метода.
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
}
}
В тесте издеваются зависимость от синхронной реализации. unit test полностью синхронный и работает в 150 мс.
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")));
}
}
Вы не проверяете асинхронное поведение, но можете проверить правильность логики.
Ответ 16
Для тех, кто любит учиться на примере, я думаю, что это хорошо:
https://github.com/playframework/play-java-ebean-example/blob/2.6.x/test/ModelTest.java