Перейти к основному содержимому

Введение в ожидание

· 4 мин. чтения

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. Тестирование с ожиданием

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

public class AsyncServiceLongRunningManualTest {
private AsyncService asyncService;

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

//...
}

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

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

Статус получает Callable , который опрашивает нашу службу через определенные промежутки времени (по умолчанию 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 и , Given . Эти методы ничего не делают и просто возвращают this , но они могут быть полезны для повышения удобочитаемости тестовых условий.

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

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

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

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

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

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

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

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

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

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

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