Awaitility для Java: полное руководство с примерами
1. Что это и зачем нужна
Awaitility — это мощная библиотека для Java, предоставляющая выразительный DSL (Domain-Specific Language) для написания асинхронных тестов. В современной разработке мы постоянно сталкиваемся с асинхронными операциями: HTTP-запросы, работа с базами данных, очереди сообщений, CompletableFuture, реактивные потоки и многое другое. Традиционные подходы к тестированию, такие как Thread.sleep(), приводят к хрупким, медленным и ненадёжным тестам. Awaitility решает эту проблему, позволяя декларативно описывать ожидания: "подожди, но не дольше 10 секунд, пока условие не станет истинным, проверяя каждые 100 миллисекунд".
Основная ценность библиотеки заключается в том, что она делает асинхронные тесты стабильными, читаемыми и производительными. Вместо того чтобы гадать, сколько времени займёт операция, вы описываете конечное состояние системы. Awaitility сам управляет временем ожидания, частотой проверок и обработкой исключений. Это особенно важно при тестировании микросервисов, интеграционных сценариев и систем с распределённой архитектурой, где время выполнения операций может сильно варьироваться.
Библиотека интегрируется со всеми популярными фреймворками тестирования (JUnit 4/5, TestNG, Spock) и может использоваться как в модульных, так и в интеграционных тестах. Awaitility не навязывает никаких специфических подходов к асинхронности — она работает с любыми механизмами: колбэки, Future, реактивные потоки, Event Bus и т.д. Единственное требование — наличие возможности проверить состояние системы в определённый момент времени.
2. Установка
Awaitility распространяется через Maven Central. Для подключения к проекту используйте менеджер зависимостей вашего проекта.
Maven (pom.xml)
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.1</version>
<scope>test</scope>
</dependency>
Gradle (build.gradle)
testImplementation 'org.awaitility:awaitility:4.2.1'
Gradle Kotlin DSL (build.gradle.kts)
testImplementation("org.awaitility:awaitility:4.2.1")
Проверка версии
Последнюю версию можно проверить на MVNRepository. На момент написания статьи актуальная стабильная версия — 4.2.1 (совместима с Java 8+).
3. Быстрый старт — минимальный рабочий пример
Рассмотрим простейший пример: у нас есть сервис, который асинхронно обновляет значение. Мы хотим дождаться, пока значение станет равным 5.
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class QuickStartTest {
@Test
public void shouldWaitForAsyncUpdate() {
// Имитируем асинхронный сервис
AtomicInteger counter = new AtomicInteger(0);
// Запускаем фоновый поток, который увеличивает счётчик
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(200); // Имитация работы
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
counter.incrementAndGet();
}
}).start();
// Awaitility ожидает, пока counter не станет равным 5
Awaitility.await()
.atMost(5, TimeUnit.SECONDS)
.until(() -> counter.get() == 5);
// Проверяем результат
assert counter.get() == 5 : "Счётчик должен быть равен 5";
}
}
Этот тест гарантированно завершится за 5 секунд (или раньше), если условие выполнится. Если же за 5 секунд счётчик не достигнет 5, тест упадёт с ConditionTimeoutException. Обратите внимание: мы не используем Thread.sleep(1000) — тест завершится сразу, как только условие станет истинным.
4. Основные методы/функции/классы
4.1. Awaitility.await() — точка входа
Сигнатура: public static Awaitility await()
Что делает: Создаёт новый экземпляр билдера для настройки и запуска ожидания. Это статический фабричный метод, с которого начинается любое использование библиотеки.
import static org.awaitility.Awaitility.await;
// Простейшее ожидание: ждём, пока условие станет true
await().until(() -> true); // Всегда завершается мгновенно
4.2. atMost() — максимальное время ожидания
Сигнатура: public Awaitility atMost(long timeout, TimeUnit unit)
Что делает: Устанавливает максимальное время, в течение которого Awaitility будет ждать выполнения условия. Если условие не выполнится за это время, будет выброшено исключение ConditionTimeoutException.
import static org.awaitility.Awaitility.await;
import java.util.concurrent.TimeUnit;
await()
.atMost(10, TimeUnit.SECONDS) // Максимум 10 секунд
.until(() -> someAsyncCondition());
4.3. pollInterval() — интервал между проверками
Сигнатура: public Awaitility pollInterval(long pollInterval, TimeUnit pollIntervalUnit)
Что делает: Определяет, как часто Awaitility будет проверять условие. По умолчанию интервал составляет 100 мс. Увеличение интервала снижает нагрузку на CPU, но замедляет реакцию на выполнение условия.
import static org.awaitility.Awaitility.await;
import java.util.concurrent.TimeUnit;
await()
.atMost(30, TimeUnit.SECONDS)
.pollInterval(1, TimeUnit.SECONDS) // Проверяем раз в секунду
.until(() -> isServiceReady());
4.4. pollDelay() — задержка перед первой проверкой
Сигнатура: public Awaitility pollDelay(long delay, TimeUnit delayUnit)
Что делает: Устанавливает задержку перед первой проверкой условия. Полезно, когда нужно дать системе время на инициализацию, прежде чем начинать проверки.
import static org.awaitility.Awaitility.await;
import java.util.concurrent.TimeUnit;
await()
.atMost(10, TimeUnit.SECONDS)
.pollDelay(2, TimeUnit.SECONDS) // Первая проверка через 2 секунды
.pollInterval(500, TimeUnit.MILLISECONDS)
.until(() -> isDatabaseConnected());
4.5. until() — основное условие ожидания
Сигнатура (основные варианты):
public void until(Callable<Boolean> condition)public <T> T until(Callable<T> supplier, Predicate<T> predicate)public <T> T until(Callable<T> supplier, Matcher<? super T> matcher)
Что делает: Принимает условие, которое должно стать истинным. Варианты с supplier и predicate/matcher позволяют проверять возвращаемое значение.
import static org.awaitility.Awaitility.await;
import java.util.concurrent.atomic.AtomicReference;
// Вариант 1: простое булево условие
await().until(() -> System.currentTimeMillis() % 2 == 0);
// Вариант 2: проверка значения с предикатом
AtomicReference<String> result = new AtomicReference<>();
await().until(() -> result.get(), value -> value != null && value.startsWith("OK"));
// Вариант 3: с Hamcrest Matcher (требуется зависимость hamcrest)
// await().until(() -> result.get(), CoreMatchers.containsString("success"));
4.6. untilAsserted() — для проверок с AssertJ/JUnit
Сигнатура: public void untilAsserted(ThrowingRunnable assertion)
Что делает: Позволяет использовать стандартные assertion-методы (из JUnit, AssertJ, Hamcrest) внутри ожидания. Awaitility будет повторять выполнение assertion до тех пор, пока он не пройдёт (не выбросит исключение) или не истечёт таймаут.
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger(0);
// Запускаем асинхронное обновление
new Thread(() -> {
try { Thread.sleep(500); } catch (InterruptedException e) {}
counter.set(42);
}).start();
await()
.atMost(2, TimeUnit.SECONDS)
.untilAsserted(() -> assertEquals(42, counter.get()));
4.7. ignoreExceptions() — игнорирование исключений
Сигнатура: public Awaitility ignoreExceptions() и public Awaitility ignoreException(Class<? extends Throwable> exceptionType)
Что делает: Позволяет игнорировать определённые исключения, которые могут возникать при проверке условия. Это полезно, когда на ранних этапах асинхронной операции ресурс ещё недоступен (например, бросает IOException или IllegalStateException).
import static org.awaitility.Awaitility.await;
import java.io.IOException;
await()
.atMost(5, TimeUnit.SECONDS)
.ignoreExceptions() // Игнорируем ВСЕ исключения
.until(() -> {
// Этот код может бросать исключения на ранних стадиях
return callExternalService();
});
// Или только конкретный тип исключения
await()
.atMost(5, TimeUnit.SECONDS)
.ignoreException(IOException.class)
.until(() -> readFromNetwork());
4.8. failFast() — быстрое завершение при ошибке
Сигнатура: public Awaitility failFast(Callable<Boolean> condition) и public Awaitility failFast(String message, Callable<Boolean> condition)
Что делает: Позволяет задать условие, при котором ожидание должно немедленно завершиться ошибкой. Это полезно для обнаружения ситуаций, когда дальнейшее ожидание бессмысленно (например, сервер вернул ошибку 500).
import static org.awaitility.Awaitility.await;
import java.util.concurrent.atomic.AtomicBoolean;
AtomicBoolean hasError = new AtomicBoolean(false);
// Имитация: через 1 секунду происходит ошибка
new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
hasError.set(true);
}).start();
await()
.atMost(10, TimeUnit.SECONDS)
.failFast(() -> hasError.get()) // Если ошибка — немедленно выходим
.until(() -> {
// Основное условие
return isDataProcessed();
});
4.9. with() — переопределение глобальных настроек
Сигнатура: public static Awaitility with()
Что делает: Позволяет настроить глобальные параметры по умолчанию для всех последующих вызовов await(). Это удобно, когда в проекте используются одинаковые настройки таймаутов и интервалов.
import static org.awaitility.Awaitility.with;
import static org.awaitility.Awaitility.await;
import java.util.concurrent.TimeUnit;
// Настраиваем глобальные параметры
with()
.pollInterval(200, TimeUnit.MILLISECONDS)
.pollDelay(100, TimeUnit.MILLISECONDS)
.await()
.atMost(30, TimeUnit.SECONDS)
.until(() -> someCondition());
// Теперь все последующие вызовы await() будут использовать эти настройки
await().until(() -> anotherCondition()); // Использует pollInterval=200ms
4.10. between() — ожидание в интервале
Сигнатура: public Awaitility between(Duration from, Duration to)
Что делает: Указывает, что условие должно выполниться в определённом временном окне. Полезно для тестирования, что операция занимает не меньше и не больше определённого времени.
import static org.awaitility.Awaitility.await;
import java.time.Duration;
await()
.between(Duration.ofSeconds(1), Duration.ofSeconds(5))
.until(() -> isCacheInvalidated());
5. Реальный практический пример (100-200 строк кода)
Рассмотрим полноценный пример тестирования REST-сервиса с асинхронной обработкой заказов. Предположим, у нас есть система, где заказ создаётся, затем асинхронно обрабатывается (статус меняется с "PENDING" на "PROCESSED" или "FAILED").
import org.awaitility.Awaitility;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.*;
/**
* Тестирование асинхронной обработки заказов.
* Демонстрирует различные возможности Awaitility.
*/
public class OrderProcessingTest {
// Имитация сервиса заказов
static class OrderService {
private final ConcurrentHashMap<String, String> orders = new ConcurrentHashMap<>();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
public String createOrder() {
String orderId = "ORD