Как использовать 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();

Он будет прокручиваться через него, поскольку все завершающие завершающие функции завершены!

Ответ 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")));
    }
}

Вы не проверяете асинхронное поведение, но можете проверить правильность логики.