Введение в Awaitlity

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 AsyncServiceTest {
    private AsyncService asyncService;

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

   //...
}

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

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

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

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

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

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

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

Awaitility также позволяет использовать hamcrest matchers для проверки результата выражения. Например, мы можем проверить, что наше значение long изменилось, как и ожидалось, после вызова метода 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 для тестирования асинхронных систем и увидели некоторые расширенные функции, которые делают библиотеку гибкой и простой в использовании в реальных проектах.

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