Как использовать JUnit для тестирования асинхронных процессов - PullRequest
166 голосов
/ 10 марта 2009

Как вы тестируете методы, которые запускают асинхронные процессы с JUnit?

Я не знаю, как заставить мой тест ждать завершения процесса (это не совсем модульный тест, это больше похоже на интеграционный тест, поскольку он включает в себя несколько классов, а не только один).

Ответы [ 15 ]

167 голосов
/ 02 декабря 2009

Альтернативой является использование класса 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 с обычным объектом в качестве блокировки, поскольку быстрые обратные вызовы могут снять блокировку до вызова метода ожидания блокировки. См. это сообщение в блоге Джо Уолнеса.

РЕДАКТИРОВАТЬ Удалены синхронизированные блоки вокруг CountDownLatch благодаря комментариям @jtahlborn и @ Ring

62 голосов
/ 22 июля 2010

Вы можете попробовать использовать библиотеку Awaitility . Это облегчает тестирование систем, о которых вы говорите.

56 голосов
/ 22 мая 2014

Если вы используете 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());
41 голосов
/ 10 марта 2009

ИМХО, это плохая практика - создавать модульные тесты или ждать их в потоках и т. Д. Вы хотели бы, чтобы эти тесты выполнялись за доли секунды. Вот почему я хотел бы предложить двухэтапный подход к тестированию асинхронных процессов.

  1. Проверьте, что ваш асинхронный процесс отправлен правильно. Вы можете смоделировать объект, который принимает ваши асинхронные запросы, и убедиться, что отправленное задание имеет правильные свойства и т. Д.
  2. Проверьте, что ваши асинхронные обратные вызовы делают правильные вещи. Здесь вы можете смоделировать изначально отправленное задание и предположить, что оно правильно инициализировано, и проверить правильность ваших обратных вызовов.
22 голосов
/ 10 марта 2009

Запустите процесс и дождитесь результата, используя Future.

17 голосов
/ 13 января 2014

Один метод, который я нашел довольно полезным для тестирования асинхронных методов, - это внедрение экземпляра Executor в конструктор объекта для теста. В производстве экземпляр 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());
}
5 голосов
/ 24 июля 2015

Нет ничего плохого в тестировании многопоточного / асинхронного кода, особенно если многопоточность составляет точку кода, который вы тестируете. Общий подход к тестированию этого материала заключается в следующем:

  • Блок основной тестовой резьбы
  • Захватить неудачные утверждения из других потоков
  • Разблокировать основной тестовый поток
  • Отбросьте все ошибки

Но это много шаблонов для одного теста. Лучше / проще - просто использовать 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: здесь .

Я также написал сообщение в блоге на эту тему для тех, кто хочет узнать немного больше.

4 голосов
/ 14 ноября 2012

Как насчет вызова SomeObject.wait и notifyAll, как описано здесь ИЛИ с использованием Роботиума Solo.waitForCondition(...) метода ИЛИ используйте класс, который я написал , чтобы сделать это (см. комментарии и тестовый класс для использования)

3 голосов
/ 23 октября 2017

Стоит отметить, что есть очень полезная глава Testing Concurrent Programs в Практика параллелизма , которая описывает некоторые подходы модульного тестирования и дает решения для проблем.

2 голосов
/ 06 декабря 2017

Избегайте тестирования с параллельными потоками, когда это возможно (что происходит чаще всего). Это только сделает ваши тесты ненадежными (иногда пройденными, иногда неудачными).

Только когда вам нужно вызвать какую-то другую библиотеку / систему, вам, возможно, придется подождать в других потоках, в этом случае всегда используйте библиотеку Awaitility вместо Thread.sleep().

Никогда просто не звоните get() или join() в ваших тестах, иначе ваши тесты могут работать вечно на вашем CI-сервере, если будущее никогда не завершится. Всегда устанавливайте isDone() первым в ваших тестах, прежде чем вызывать get(). Для CompletionStage это .toCompletableFuture().isDone().

Когда вы тестируете неблокирующий метод, подобный этому:

public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
    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), то в ваших тестах внедряется однопотоковая служба ExecutorService, например, из guava:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Если в вашем коде используется forkJoinPool, например thenApplyAsync(...), перепишите код для использования ExecutorService (есть много веских причин) или используйте Awaitility.

Чтобы сократить пример, я сделал BarService аргументом метода, реализованным как лямбда-код Java8 в тесте, обычно это была бы вставленная ссылка, которую вы бы высмеяли.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...