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

Тестирование реактивных потоков с помощью StepVerifier и TestPublisher

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

1. Обзор

В этом руководстве мы подробно рассмотрим тестирование реактивных потоков с помощью StepVerifier и TestPublisher .

Мы будем основывать наше исследование на приложении Spring Reactor , содержащем цепочку операций реактора.

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

Spring Reactor поставляется с несколькими классами для тестирования реактивных потоков.

Мы можем получить их, добавив зависимость реактора- теста :

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
    <version>3.2.3.RELEASE</version>
</dependency>

3. Степверификатор

В общем, реакторные испытания имеют два основных применения:

  • создание пошагового теста с помощью StepVerifier
  • создание предопределенных данных с помощью TestPublisher для тестирования нижестоящих операторов

Наиболее распространенный случай тестирования реактивных потоков — это когда у нас есть издатель ( Flux или Mono ), определенный в нашем коде. Мы хотим знать, как он ведет себя, когда кто-то подписывается.

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

Прежде всего, давайте создадим издатель с некоторыми операторами.

Мы будем использовать элементы Flux.just(T). Этот метод создаст поток , который испускает заданные элементы, а затем завершается.

Поскольку расширенные операторы выходят за рамки этой статьи, мы просто создадим простой издатель, который выводит только четырехбуквенные имена, отображаемые в верхнем регистре:

Flux<String> source = Flux.just("John", "Monica", "Mark", "Cloe", "Frank", "Casper", "Olivia", "Emily", "Cate")
.filter(name -> name.length() == 4)
.map(String::toUpperCase);

3.1. Пошаговый сценарий

Теперь давайте проверим наш источник с помощью StepVerifier , чтобы проверить, что произойдет, когда кто-то подпишется :

StepVerifier
.create(source)
.expectNext("JOHN")
.expectNextMatches(name -> name.startsWith("MA"))
.expectNext("CLOE", "CATE")
.expectComplete()
.verify();

Сначала мы создаем построитель StepVerifier с помощью метода create .

Далее мы оборачиваем наш тестируемый источник Flux . Первый сигнал проверяется с помощью expectNext(T element), но на самом деле мы можем передать любое количество элементов в expectNext .

Мы также можем использовать expectNextMatches и предоставить Predicate<T> для более точного соответствия.

Что касается нашего последнего ожидания, мы ожидаем, что наш поток завершится.

И, наконец, мы используем verify() для запуска нашего теста .

3.2. Исключения в StepVerifier

Теперь давайте объединим нашего издателя Flux с Mono .

У нас будет немедленное завершение этого Mono с ошибкой при подписке на :

Flux<String> error = source.concatWith(
Mono.error(new IllegalArgumentException("Our message"))
);

Теперь, после четырех всех элементов, мы ожидаем, что наш поток завершится с исключением :

StepVerifier
.create(error)
.expectNextCount(4)
.expectErrorMatches(throwable -> throwable instanceof IllegalArgumentException &&
throwable.getMessage().equals("Our message")
).verify();

Мы можем использовать только один метод для проверки исключений. Сигнал OnError уведомляет подписчика о том, что издатель закрыт с ошибкой. Поэтому мы не можем добавить больше ожиданий позже .

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

  • expectError() – ожидать любую ошибку
  • expectError(Class<? extends Throwable> clazz ) — ожидать ошибку определенного типа
  • expectErrorMessage(String errorMessage) – ожидать ошибку, имеющую конкретное сообщение
  • expectErrorMatches(Predicate<Throwable> predicate) – ожидать ошибку, которая соответствует заданному предикату.
  • expectErrorSatisfies(Consumer<Throwable> assertionConsumer) — потребляет Throwable , чтобы выполнить пользовательское утверждение.

3.3. Тестирование издателей, основанных на времени

Иногда наши издатели зависят от времени.

Например, предположим, что в нашем реальном приложении у нас есть однодневная задержка между событиями . Теперь, очевидно, мы не хотим, чтобы наши тесты выполнялись целый день, чтобы проверить ожидаемое поведение с такой задержкой.

Конструктор StepVerifier.withVirtualTime разработан, чтобы избежать длительных тестов.

Мы создаем билдер, вызывая withVirtualTime . Обратите внимание, что этот метод не использует Flux `` в качестве входных данных. Вместо этого он использует Supplier , который лениво создает экземпляр тестируемого Flux после настройки планировщика.

Чтобы продемонстрировать, как мы можем проверить ожидаемую задержку между событиями, давайте создадим Flux с интервалом в одну секунду, который выполняется в течение двух секунд. Если таймер работает правильно, мы должны получить только два элемента:

StepVerifier
.withVirtualTime(() -> Flux.interval(Duration.ofSeconds(1)).take(2))
.expectSubscription()
.expectNoEvent(Duration.ofSeconds(1))
.expectNext(0L)
.thenAwait(Duration.ofSeconds(1))
.expectNext(1L)
.verifyComplete();

Обратите внимание, что нам следует избегать создания экземпляра Flux ранее в коде, а затем поставщика , возвращающего эту переменную. Вместо этого мы всегда должны создавать экземпляр Flux внутри лямбды.

Есть два основных метода ожидания, которые имеют дело со временем:

  • thenAwait(Duration duration) – приостанавливает оценку шагов; за это время могут произойти новые события
  • expectNoEvent(Duration duration) — сбой, когда какое-либо событие появляется в течение промежутка времени ; последовательность будет проходить с заданной продолжительностью

Обратите внимание, что первым сигналом является событие подписки, поэтому каждому expectNoEvent(Duration duration) должно предшествовать expectSubscription() .

3.4. Утверждения после выполнения с помощью StepVerifier

Итак, как мы видели, несложно шаг за шагом описать наши ожидания.

Однако иногда нам нужно проверить дополнительное состояние после успешного завершения всего нашего сценария.

Давайте создадим собственный издатель. Он выдаст несколько элементов, затем завершится, приостановится и выпустит еще один элемент, который мы отбросим :

Flux<Integer> source = Flux.<Integer>create(emitter -> {
emitter.next(1);
emitter.next(2);
emitter.next(3);
emitter.complete();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
emitter.next(4);
}).filter(number -> number % 2 == 0);

Мы ожидаем, что он выдаст 2, но сбросит 4, так как мы сначала вызвали emitter.complete .

Итак, давайте проверим это поведение с помощью verifyThenAssertThat. Этот метод возвращает StepVerifier.Assertions, на которые мы можем добавить наши утверждения:

@Test
public void droppedElements() {
StepVerifier.create(source)
.expectNext(2)
.expectComplete()
.verifyThenAssertThat()
.hasDropped(4)
.tookLessThan(Duration.ofMillis(1050));
}

4. Создание данных с помощью TestPublisher

Иногда нам могут понадобиться некоторые специальные данные для запуска выбранных сигналов.

Например, у нас может быть очень конкретная ситуация, которую мы хотим проверить.

В качестве альтернативы мы можем реализовать собственный оператор и проверить, как он себя ведет.

В обоих случаях мы можем использовать TestPublisher<T> , что позволяет нам программно запускать различные сигналы:

  • next(T value) или next(T value, T rest) – отправить один или несколько сигналов подписчикам
  • emit(T value) – то же, что и next(T) , но после этого вызывает complete()
  • complete () — завершает источник полным сигналом
  • error(Throwable tr) — завершает источник с ошибкой
  • flux() — удобный способ обернуть TestPublisher во Flux .
  • mono() — то же самое, что и flux() , но с преобразованием в Mono

4.1. Создание TestPublisher

Давайте создадим простой TestPublisher , который выдает несколько сигналов, а затем завершается с исключением:

TestPublisher
.<String>create()
.next("First", "Second", "Third")
.error(new RuntimeException("Message"));

4.2. TestPublisher в действии

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

В этом случае особенно важно, чтобы мы полностью владели источником данных. Чтобы добиться этого, мы снова можем положиться на TestPublisher .

Во-первых, давайте создадим класс, который использует Flux<String> в качестве параметра конструктора для выполнения операции getUpperCase() :

class UppercaseConverter {
private final Flux<String> source;

UppercaseConverter(Flux<String> source) {
this.source = source;
}

Flux<String> getUpperCase() {
return source
.map(String::toUpperCase);
}
}

Предположим, что UppercaseConverter — это наш класс со сложной логикой и операторами, и нам нужно предоставить очень конкретные данные от исходного издателя.

Мы можем легко добиться этого с помощью TestPublisher:

final TestPublisher<String> testPublisher = TestPublisher.create();

UppercaseConverter uppercaseConverter = new UppercaseConverter(testPublisher.flux());

StepVerifier.create(uppercaseConverter.getUpperCase())
.then(() -> testPublisher.emit("aA", "bb", "ccc"))
.expectNext("AA", "BB", "CCC")
.verifyComplete();

В этом примере мы создаем тестовый издатель Flux в параметре конструктора UppercaseConverter . Затем наш TestPublisher выдает три элемента и завершает работу.

4.3. Плохое поведение TestPublisher ``

С другой стороны, мы можем создать плохо работающий TestPublisher с помощью фабричного метода createNonCompliant . Нам нужно передать в конструктор одно значение перечисления из TestPublisher.Violation. Эти значения указывают, какие части спецификаций могут быть пропущены нашим издателем.

Давайте взглянем на TestPublisher , который не будет генерировать исключение NullPointerException для нулевого элемента:

TestPublisher
.createNoncompliant(TestPublisher.Violation.ALLOW_NULL)
.emit("1", "2", null, "3");

В дополнение к ALLOW_NULL мы также можем использовать TestPublisher.Violation для:

  • REQUEST_OVERFLOW — позволяет вызывать next() без создания исключения IllegalStateException при недостаточном количестве запросов.
  • CLEANUP_ON_TERMINATE — позволяет отправить любой сигнал завершения несколько раз подряд
  • DEFER_CANCELATION — позволяет игнорировать сигналы отмены и продолжать испускание элементов .

5. Вывод

В этой статье мы обсудили различные способы тестирования реактивных потоков из проекта Spring Reactor .

Сначала мы увидели, как использовать StepVerifier для тестирования издателей. Затем мы увидели, как использовать TestPublisher. Точно так же мы видели, как работать с TestPublisher , который ведет себя неправильно .

Как обычно, реализацию всех наших примеров можно найти в проекте Github .