Как использовать Junit для тестирования асинхронных процессов



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



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

841   15  

15 ответов:

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

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

EDIT удалены синхронизированные блоки вокруг CountDownLatch благодаря комментариям от @jtahlborn и @Ring

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

Если вы используете 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());

запустите процесс и дождитесь результата с помощью Future.

один метод, который я нашел довольно полезным для тестирования асинхронных методов, - это инъекция 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() экземпляр исполнителя в то время как в тесте я бы, вероятно, построить его с синхронным исполнителем, который делает следующее --

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

как позвонить SomeObject.wait и notifyAll Как рассказали здесь или с помощью RobotiumsSolo.waitForCondition(...) метод или использовать класс, я писал чтобы сделать это (см. комментарии и тестовый класс для использования)

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

  • блокировать основной тестовый поток
  • захват неудачных утверждений из других потоков
  • разблокировать основной тестовый поток
  • генерация каких-либо сбоев

но это много шаблонных для одного теста. А лучше / проще подход заключается в том, чтобы просто использовать 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-это здесь.

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

Я предпочитаю использовать 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];
    }
}

в принципе, нам нужно создать окончательную ссылку на массив, которая будет использоваться внутри анонимного внутреннего класса. Я бы предпочел создать логическое [], потому что я могу поставить значение для управления, если нам нужно ждать(). Когда все будет сделано, мы просто выпустим asyncExecuted.

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

Я нахожу библиотеку 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 для блокировки, пока не получите результат так же, как синхронный способ. И установите тайм-аут, чтобы не предполагать слишком много времени, чтобы ждать результата.

здесь есть много ответов, но простой - просто создать завершенный 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();

он будет zip прямо через него, как все CompletableFutures закончены!

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

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

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

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

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

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

чтобы сделать это, проверьте с незавершенным будущим, которое вы установили в завершено вручную:

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

таким образом, если вы добавляете future.join() для doSomething (), тест не удастся.

если ваша служба использует ExecutorService, например, в thenApplyAsync(..., executorService), затем в ваши тесты вводится однопоточный ExecutorService, например, из guava:

ExecutorService executorService = Executors.newSingleThreadExecutor();

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

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

Если вы хотите проверить логику, просто не проверяйте ее асинхронно.

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

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

в тесте макет зависимости с синхронной реализацией. Модульный тест полностью синхронный и работает в 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")));
    }
}

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

это то, что я использую в настоящее время, если результат теста производится асинхронно.

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 на провал рано.

Comments

    Ничего не найдено.