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

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

1. обзор

В этом руководстве мы подробно рассмотрим тестированиеreactive streams сStepVerifier иTestPublisher.

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

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

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

Мы можем получить их, добавивthe reactor-test dependency:


    io.projectreactor
    reactor-test
    test
    3.2.3.RELEASE

3. StepVerifierс

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

  • создание пошагового теста сStepVerifier

  • создание предопределенных данных с помощью следующих операторовTestPublisher to test

Наиболее распространенный случай тестирования реактивных потоков - это когда в нашем коде определен издатель (aFlux orMono). We want to know how it behaves when someone subscribes. 

С помощью APIStepVerifier мы можем определить наши ожидания опубликованных элементов в видеwhat elements we expect and what happens when our stream completes.

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

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

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

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

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

Теперь давайте проверим нашsource w сStepVerifierin order to test what will happen when someone subscribes:

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

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

Затем мы обертываем нашFlux source, который находится в стадии тестирования. Первый сигнал проверяетсяexpectNext(T element), , но на самом делеwe can pass any number of elements to expectNext.

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

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

И, наконец,we use verify() to trigger our test.

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

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

We’ll have this Mono terminate immediately with an error when subscribed to:

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

Теперь, после четырех всех элементов,we expect our stream to terminate with an exception:

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

We can use only one method to verify exceptions. СигналOnError уведомляет абонента о том, чтоthe publisher is closed with an error state. Therefore, we can’t add more expectations afterward.

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

  • expectError() – ожидать любой ошибки

  • expectError(Class<? extends Throwable> clazz) – определить ошибку определенного типа

  • expectErrorMessage(String errorMessage) – expect ошибки с конкретным сообщением

  • expectErrorMatches(Predicate<Throwable> predicate) – ожидать ошибки, соответствующей заданному предикату

  • expectErrorSatisfies(Consumer<Throwable> assertionConsumer) – использоватьThrowable in order для выполнения пользовательского утверждения

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

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

Например, предположим, что в нашем реальном приложенииwe have a one-day delay between events. Теперь очевидно, что мы не хотим, чтобы наши тесты выполнялись в течение всего дня, чтобы проверить ожидаемое поведение с такой задержкой.

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

We create a builder by calling withVirtualTime.Note that this method doesn’t take Flux as input. Вместо этого требуетсяSupplier, который лениво создает экземпляр проверенногоFlux after с установленным планировщиком.

Чтобы продемонстрировать, как мы можем проверить ожидаемую задержку между событиями, давайте создадимFlux w с интервалом в одну секунду, который выполняется в течение двух секунд. If the timer runs correctly, we should only get two elements:

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

Обратите внимание, что нам следует избегать создания экземпляра поисковикаFlux в коде и последующего возврата этой переменнойSupplier . Вместо этогоwe should always instantiate Flux inside the lambda.

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

  • thenAwait(Duration duration) – приостанавливает оценку шагов; в это время могут произойти новые события

  • expectNoEvent(Duration duration) – выдает ошибку, если в течениеduration происходит какое-либо событие; последовательность будет проходить с заданнымduration

Обратите внимание, что первый сигнал - это событие подписки, поэтомуevery expectNoEvent(Duration duration) should be preceded with *expectSubscription()*.

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

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

Однакоsometimes we need to verify additional state after our whole scenario played out successfully.

Давайте создадим собственного издателя. It will emit a few elements, then complete, pause, and emit one more element, which we’ll drop:

Flux source = Flux.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 on, к которому мы можем добавить наши утверждения:

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

4. Получение данных сTestPublisher

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

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

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

В обоих случаях можно использоватьTestPublisher<T>, чтоallows us to programmatically trigger miscellaneous signals:

  • next(T value) илиnext(T value, T rest) –  отправляют один или несколько сигналов подписчикам

  • emit(T value) – то же, что иnext(T) , но после этого вызываетcomplete()

  • complete() - завершает источник сигналомcomplete

  • error(Throwable tr) – стерилизует источник с ошибкой

  • flux() – удобный способ обернутьTestPublisher intoFlux

  • mono() – тот же usflux() , но превращается в aMono

4.1. СозданиеTestPublisher

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

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

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

Как мы упоминали ранее, иногда нам может потребоваться запуститьa finely chosen signal that closely matches to a particular situation.

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

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

class UppercaseConverter {
    private final Flux source;

    UppercaseConverter(Flux source) {
        this.source = source;
    }

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

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

Этого легко добиться с помощьюTestPublisher:

final TestPublisher 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 publisher в параметре sconstructorUppercaseConverter . Затем нашTestPublisher испускает три элемента и завершается.

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

С другой стороны,we can create a misbehaving TestPublisher with the createNonCompliant factory method.. Нам нужно передать в конструктор одно значение перечисления изTestPublisher.Violation.. Эти значения определяют, какие части спецификаций наш издатель может пропустить.

Давайте посмотрим наTestPublisher t, который не выдастNullPointerException  для элементаnull:

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

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

  • REQUEST_OVERFLOW - позволяет вызватьnext() w без выдачиIllegalStateException при недостаточном количестве запросов

  • CLEANUP_ON_TERMINATE – разрешает посылать любой сигнал завершения несколько раз подряд

  • DEFER_CANCELLATION – позволяет нам игнорировать сигналы отмены и продолжать испускать элементы

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

В этой статьеwe discussed various ways of testing reactive streams from the Spring Reactor project.

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

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