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 .