«1. Введение

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

В этой статье мы рассмотрим Awaitility — библиотеку, предоставляющую простой предметно-ориентированный язык (DSL) для тестирования асинхронных систем.

С Awaitility мы можем выразить наши ожидания от системы в удобном для чтения DSL.

2. Зависимости

Нам нужно добавить зависимости Awaitility в наш pom.xml.

Библиотеки awaitility будет достаточно для большинства случаев использования. Если мы хотим использовать условия на основе прокси, нам также необходимо предоставить библиотеку awaitility-proxy:

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility-proxy</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>

Вы можете найти последнюю версию библиотек awaitility и awaitility-proxy на Maven Central.

3. Создание асинхронной службы

Давайте напишем простую асинхронную службу и протестируем ее:

public class AsyncService {
    private final int DELAY = 1000;
    private final int INIT_DELAY = 2000;

    private AtomicLong value = new AtomicLong(0);
    private Executor executor = Executors.newFixedThreadPool(4);
    private volatile boolean initialized = false;

    void initialize() {
        executor.execute(() -> {
            sleep(INIT_DELAY);
            initialized = true;
        });
    }

    boolean isInitialized() {
        return initialized;
    }

    void addValue(long val) {
        throwIfNotInitialized();
        executor.execute(() -> {
            sleep(DELAY);
            value.addAndGet(val);
        });
    }

    public long getValue() {
        throwIfNotInitialized();
        return value.longValue();
    }

    private void sleep(int delay) {
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
        }
    }

    private void throwIfNotInitialized() {
        if (!initialized) {
            throw new IllegalStateException("Service is not initialized");
        }
    }
}

4. Тестирование с помощью Awaitility

Теперь давайте создадим тестовый класс:

public class AsyncServiceLongRunningManualTest {
    private AsyncService asyncService;

    @Before
    public void setUp() {
        asyncService = new AsyncService();
    }
    
    //...
}

Наш тест проверяет, происходит ли инициализация нашего сервиса в течение заданного периода времени (по умолчанию 10 с) после вызова метода инициализации.

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

Статус получает вызываемый объект, который опрашивает нашу службу через определенные промежутки времени (по умолчанию 100 мс) после указанной начальной задержки (по умолчанию 100 мс). Здесь мы используем настройки по умолчанию для времени ожидания, интервала и задержки:

asyncService.initialize();
await()
  .until(asyncService::isInitialized);

Здесь мы используем await — один из статических методов класса Awaitility. Он возвращает экземпляр класса ConditionFactory. Мы также можем использовать другие методы, подобные приведенным, для повышения удобочитаемости.

Параметры времени по умолчанию можно изменить с помощью статических методов класса Awaitility:

Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS);
Awaitility.setDefaultPollDelay(Duration.ZERO);
Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);

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

Мы также можем предоставить пользовательские значения времени для каждого вызова await. Здесь мы ожидаем, что инициализация произойдет максимум через пять секунд и, по крайней мере, через 100 мс с интервалами опроса в 100 мс:

asyncService.initialize();
await()
    .atLeast(Duration.ONE_HUNDRED_MILLISECONDS)
    .atMost(Duration.FIVE_SECONDS)
  .with()
    .pollInterval(Duration.ONE_HUNDRED_MILLISECONDS)
    .until(asyncService::isInitialized);

Стоит отметить, что ConditionFactory содержит дополнительные методы, такие как with, then и, задано. Эти методы ничего не делают и просто возвращают это, но они могут быть полезны для повышения удобочитаемости тестовых условий.

5. Использование сопоставителей

Awaitility также позволяет использовать сопоставители подколенного сухожилия для проверки результата выражения. Например, мы можем проверить, что наше длинное значение изменилось, как и ожидалось, после вызова метода addValue:

asyncService.initialize();
await()
  .until(asyncService::isInitialized);
long value = 5;
asyncService.addValue(value);
await()
  .until(asyncService::getValue, equalTo(value));

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

6. Игнорирование исключений

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

Awaitility предоставляет возможность игнорировать это исключение без провала теста.

Например, давайте проверим, что результат getValue равен нулю сразу после инициализации, игнорируя IllegalStateException:

asyncService.initialize();
given().ignoreException(IllegalStateException.class)
  .await().atMost(Duration.FIVE_SECONDS)
  .atLeast(Duration.FIVE_HUNDRED_MILLISECONDS)
  .until(asyncService::getValue, equalTo(0L));

7. Использование прокси использовать условия на основе прокси. Идея проксирования заключается в предоставлении реальных вызовов методов для условий без реализации Callable или лямбда-выражения.

Давайте воспользуемся статическим методом AwaitilityClassProxy.to, чтобы проверить, что AsyncService инициализирован:

8. Доступ к полям

asyncService.initialize();
await()
  .untilCall(to(asyncService).isInitialized(), equalTo(true));

Awaitility может даже обращаться к закрытым полям для выполнения над ними утверждений. В следующем примере мы можем увидеть другой способ получить статус инициализации нашего сервиса:

9. Заключение

asyncService.initialize();
await()
  .until(fieldIn(asyncService)
  .ofType(boolean.class)
  .andWithName("initialized"), equalTo(true));

«В этом кратком руководстве мы представили библиотеку Awaitility, познакомились с ее базовым DSL для тестирования асинхронных систем и увидели некоторые дополнительные функции, которые делают библиотеку гибкой и простой в использовании в реальных проектах.

Как всегда, все примеры кода доступны на Github.

«