Awaitility для Java: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                    

Рекомендаций