Як ви перевіряєте методи, які запускають асинхронні процеси з JUnit?
Я не знаю, як змусити тест чекати, коли процес закінчиться (це не зовсім одиничний тест, він більше схожий на тест інтеграції, оскільки він включає кілька класів, а не один).
Як ви перевіряєте методи, які запускають асинхронні процеси з JUnit?
Я не знаю, як змусити тест чекати, коли процес закінчиться (це не зовсім одиничний тест, він більше схожий на тест інтеграції, оскільки він включає кілька класів, а не один).
Відповіді:
IMHO - це погана практика створювати одиничні тести або чекати на потоках тощо. Ви б хотіли, щоб ці тести виконувались за секунди. Ось чому я хотів би запропонувати двоступеневий підхід до тестування асинхронних процесів.
Альтернативою є використання класу 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());
}
}
ПРИМІТКА, ви не можете просто використовувати синхронізований звичайний об'єкт як замок, оскільки швидкі зворотні виклики можуть звільнити замок до виклику методу очікування блокування. Дивіться цю публікацію в блозі Джо Уолнес.
EDIT Видалено синхронізовані блоки навколо CountDownLatch завдяки коментарям від @jtahlborn та @Ring
Ви можете спробувати скористатися бібліотекою « Чекання» . Це дозволяє легко протестувати системи, про які ви говорите.
CountDownLatch
(див. Відповідь від @Martin) краще в цьому плані.
Якщо ви використовуєте 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());
Почніть процес вимкнення і чекайте результату, використовуючи a Future
.
Один із методів, який я знайшов досить корисним для тестування асинхронних методів, - це введення 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());
}
WebClient
З тестуванням потокового / асинхронного коду немає нічого по суті, особливо якщо введення теми - це тест коду, який ви тестуєте. Загальним підходом до тестування цього матеріалу є:
Але це дуже багато котлован для одного тесту. Кращий / простіший підхід - просто використовувати 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 знаходиться тут .
Я також написав допис у блозі на цю тему для тих, хто хоче дізнатися трохи детальніше.
Як щодо виклику SomeObject.wait
та notifyAll
як описано тут АБО за допомогою методу Robotiums Solo.waitForCondition(...)
АБО використовуйте клас, про який я писав (див. Коментарі та тестовий клас щодо використання)
Я знаходжу бібліотеку 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 для блокування, поки ви не отримаєте результат так само, як синхронний шлях. І встановіть тайм-аут, щоб не припускати занадто багато часу для очікування результату.
Варто відзначити , що є дуже корисна глава Testing Concurrent Programs
в Паралелізм на практиці , яка описує деякі одиниці тестування підходів і дає рішення проблем.
Це я зараз використовую, якщо результат тестування створюється асинхронно.
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
можете скоріше відмовитись.
Тут є багато відповідей, але простий - просто створити завершений файл 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();
Він буде прошивати прямо через нього, як всі завершені майбутні завершено!
Уникайте тестування з паралельними нитками, коли це можливо (це більшість часу). Це лише зробить ваші тести розмитими (іноді проходять, іноді виходять з ладу).
Тільки коли вам потрібно зателефонувати в якусь іншу бібліотеку / систему, можливо, доведеться чекати на інших потоках , у такому випадку завжди використовуйте бібліотеку « Чекання» замість Thread.sleep()
.
Ніколи не телефонуйте get()
або join()
в своїх тестах, інакше ваші тести можуть працювати назавжди на вашому сервері CI у випадку, якщо майбутнє ніколи не завершиться. Завжди запевняйте isDone()
в своїх тестах, перш ніж дзвонити get()
. Для CompletionStage, тобто .toCompletableFuture().isDone()
.
Коли ви перевіряєте такий метод, який не блокує:
public static CompletionStage<String> createGreeting(CompletableFuture<String> future) {
return future.thenApply(result -> "Hello " + result);
}
тоді ви не повинні просто тестувати результат, передаючи завершене майбутнє в тесті, ви також повинні переконатися, що ваш метод 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 в тесті, як правило, це буде введена посилання, яку ви знущаєтеся.
Я вважаю за краще використовувати зачекати та сповістити. Це просто і зрозуміло.
@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.
Для всіх користувачів Spring, саме зараз я зазвичай роблю свої інтеграційні тести, в яких бере участь асинхронна поведінка:
Запустіть подію програми у виробничому коді, коли завдання асинхронізації (наприклад, дзвінок вводу-виводу) завершено. Більшу частину часу ця подія так чи інакше потрібна для обробки відповіді на операцію асинхронізації у виробництві.
Коли ця подія відбулася, ви можете використовувати таку стратегію у своєму тестовому випадку:
Щоб вирішити цю проблему, спочатку вам знадобиться якась подія домену для запуску. Я тут використовую UUID, щоб визначити завдання, яке було виконано, але ви, звичайно, можете використовувати щось інше, поки це унікально.
(Зауважте, що наведені нижче фрагменти коду також використовують анотації Lombok, щоб позбутися коду пластини котла)
@RequiredArgsConstructor
class TaskCompletedEvent() {
private final UUID taskId;
// add more fields containing the result of the task if required
}
Сам виробничий код зазвичай виглядає так:
@Component
@RequiredArgsConstructor
class Production {
private final ApplicationEventPublisher eventPublisher;
void doSomeTask(UUID taskId) {
// do something like calling a REST endpoint asynchronously
eventPublisher.publishEvent(new TaskCompletedEvent(taskId));
}
}
Тоді я можу використовувати Spring, @EventListener
щоб зловити опубліковану подію в тестовому коді. Слухач події дещо більше задіяний, тому що йому належить безпечно обробляти два випадки:
A CountDownLatch
використовується для другого випадку, як згадується в інших відповідях тут. Також зауважте, що @Order
анотація про метод обробника подій гарантує, що цей метод обробника подій викликається після всіх інших слухачів подій, які використовуються у виробництві.
@Component
class TaskCompletionEventListener {
private Map<UUID, CountDownLatch> waitLatches = new ConcurrentHashMap<>();
private List<UUID> eventsReceived = new ArrayList<>();
void waitForCompletion(UUID taskId) {
synchronized (this) {
if (eventAlreadyReceived(taskId)) {
return;
}
checkNobodyIsWaiting(taskId);
createLatch(taskId);
}
waitForEvent(taskId);
}
private void checkNobodyIsWaiting(UUID taskId) {
if (waitLatches.containsKey(taskId)) {
throw new IllegalArgumentException("Only one waiting test per task ID supported, but another test is already waiting for " + taskId + " to complete.");
}
}
private boolean eventAlreadyReceived(UUID taskId) {
return eventsReceived.remove(taskId);
}
private void createLatch(UUID taskId) {
waitLatches.put(taskId, new CountDownLatch(1));
}
@SneakyThrows
private void waitForEvent(UUID taskId) {
var latch = waitLatches.get(taskId);
latch.await();
}
@EventListener
@Order
void eventReceived(TaskCompletedEvent event) {
var taskId = event.getTaskId();
synchronized (this) {
if (isSomebodyWaiting(taskId)) {
notifyWaitingTest(taskId);
} else {
eventsReceived.add(taskId);
}
}
}
private boolean isSomebodyWaiting(UUID taskId) {
return waitLatches.containsKey(taskId);
}
private void notifyWaitingTest(UUID taskId) {
var latch = waitLatches.remove(taskId);
latch.countDown();
}
}
Останнім кроком є виконання тестової системи в тестовому випадку. Тут я використовую тест SpringBoot з JUnit 5, але це має працювати однаково для всіх тестів, що використовують Spring-контекст.
@SpringBootTest
class ProductionIntegrationTest {
@Autowired
private Production sut;
@Autowired
private TaskCompletionEventListener listener;
@Test
void thatTaskCompletesSuccessfully() {
var taskId = UUID.randomUUID();
sut.doSomeTask(taskId);
listener.waitForCompletion(taskId);
// do some assertions like looking into the DB if value was stored successfully
}
}
Зауважте, що на відміну від інших відповідей тут, це рішення також буде працювати, якщо ви будете виконувати свої тести паралельно і кілька потоків одночасно здійснювати код асинхронізації.
Якщо ви хочете перевірити логіку, не перевіряйте її асинхронно.
Наприклад, для перевірки цього коду, який працює на результатах асинхронного методу.
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")));
}
}
Ви не перевіряєте асинхронну поведінку, але можете перевірити, чи логіка правильна.