Тестване на реактивни потоци с помощта на StepVerifier и TestPublisher

1. Общ преглед

В този урок ще разгледаме отблизо тестването на реактивни потоци с StepVerifier и TestPublisher .

Ще основаваме разследването си на приложение Spring Spring, съдържащо верига от реакторни операции.

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

Spring Reactor се предлага с няколко класа за тестване на реактивни потоци.

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

 io.projectreactor reactor-test test     3.2.3.RELEASE 

3. StepVerifier

Като цяло, реакторът има две основни приложения:

  • създаване на тест стъпка по стъпка с StepVerifier
  • създаване на предварително дефинирани данни с TestPublisher за тестване на оператори надолу по веригата

Най-честият случай при тестване на реактивни потоци е, когато имаме издател ( Flux или Mono ), дефиниран в нашия код. Искаме да знаем как се държи, когато някой се абонира.

С API на StepVerifier можем да дефинираме нашите очаквания за публикувани елементи по отношение на това какви елементи очакваме и какво се случва, когато потокът ни завърши .

Първо, нека създадем издател с някои оператори.

Ще използваме Flux.just (Т елементи). Този метод ще създаде Flux, който излъчва дадени елементи и след това завършва.

Тъй като напредналите оператори са извън обхвата на тази статия, ние просто ще създадем прост издател, който извежда само имена от четири букви, съпоставени с главни букви:

Flux 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 , който е в процес на изпитване. Първият сигнал се проверява с awaNext (T елемент), но наистина можем да предадем произволен брой елементи, за да очакваме Next .

Можем също да използваме awaNextMatches и да предоставим предикат за по-персонализирано съвпадение.

За последното ни очакване очакваме, че потокът ни завърши.

И накрая, използваме verify (), за да задействаме нашия тест .

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

Сега, нека обединим нашия издател на Flux с Mono .

Ще направим това Mono незабавно да се прекрати с грешка, когато се абонирате за :

Flux 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 уведомява абоната, че издателят е затворен със състояние на грешка. Следователно не можем да добавим повече очаквания след това .

Ако не е необходимо да проверите вида и съобщението на изключението наведнъж, тогава можем да използваме един от специалните методи:

  • awakeError () - очаквайте всякакъв вид грешка
  • awactError (Class clazz ) - очаквайте грешка от определен тип
  • expeEErrorMessage (String errorMessage) - очаквайте грешка със специфично съобщение
  • awakeErrorMatches (предикат предикат) - очаквайте грешка, която съвпада с даден предикат
  • awakeErrorSatisfies (Consumer assertionConsumer) - консумира Throwable, за да направи потребителско твърдение

3.3. Тестване на издатели, базирани на време

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

Да предположим например, че в нашето приложение от реалния живот имаме еднодневно забавяне между събитията . Сега, очевидно, не искаме тестовете ни да работят цял ​​ден, за да проверят очакваното поведение с такова закъснение.

Конструкторът StepVerifier.withVirtualTime е проектиран да избягва продължителни тестове.

Създаваме конструктор, като се обаждаме сVirtualTime . Имайте предвид, че този метод не приема 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) – pauses the evaluation of the steps; new events may occur during this time
  • expectNoEvent(Duration duration) – fails when any event appears during the duration; the sequence will pass with a given duration

Please notice that the first signal is the subscription event, so every expectNoEvent(Duration duration) should be preceded with expectSubscription().

3.4. Post-Execution Assertions with StepVerifier

So, as we've seen, it's straightforward to describe our expectations step-by-step.

However, sometimes we need to verify additional state after our whole scenario played out successfully.

Let's create a custom publisher. 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);

We expect that it will emit a 2, but drop a 4, since we called emitter.complete first.

So, let's verify this behavior by using verifyThenAssertThat. This method returns StepVerifier.Assertions on which we can add our assertions:

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

4. Producing Data with TestPublisher

Sometimes, we might need some special data in order to trigger the chosen signals.

For instance, we may have a very particular situation that we want to test.

Alternatively, we may choose to implement our own operator and want to test how it behaves.

For both cases, we can use TestPublisher, which allows us to programmatically trigger miscellaneous signals:

  • next(T value) or next(T value, T rest) – send one or more signals to subscribers
  • emit(T value) – same as next(T) but invokes complete() afterwards
  • complete() – terminates a source with the complete signal
  • error(Throwable tr) – terminates a source with an error
  • flux() – convenient method to wrap a TestPublisher into Flux
  • mono() – same us flux() but wraps to a Mono

4.1. Creating a TestPublisher

Let's create a simple TestPublisher that emits a few signals and then terminates with an exception:

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

4.2. TestPublisher in Action

As we mentioned earlier, we may sometimes want to trigger a finely chosen signal that closely matches to a particular situation.

Now, it's especially important in this case that we have complete mastery over the source of the data. To achieve this, we can again rely on TestPublisher.

First, let's create a class that uses Flux as the constructor parameter to perform the operation getUpperCase():

class UppercaseConverter { private final Flux source; UppercaseConverter(Flux source) { this.source = source; } Flux getUpperCase() { return source .map(String::toUpperCase); } }

Suppose that UppercaseConverter is our class with complex logic and operators, and we need to supply very particular data from the source publisher.

We can easily achieve this with 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();

In this example, we create a test Flux publisher in the UppercaseConverter constructor parameter. Then, our TestPublisher emits three elements and completes.

4.3. Misbehaving TestPublisher

On the other hand, we can create a misbehaving TestPublisher with the createNonCompliant factory method. We need to pass in the constructor one enum value from TestPublisher.Violation. These values specify which parts of specifications our publisher may overlook.

Let's take a look at a TestPublisher that won't throw a NullPointerException for the null element:

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

In addition to ALLOW_NULL, we can also use TestPublisher.Violation to:

  • REQUEST_OVERFLOW – allows calling next() without throwing an IllegalStateException when there's an insufficient number of requests
  • CLEANUP_ON_TERMINATE – allows sending any termination signal several times in a row
  • DEFER_CANCELLATION – allows us to ignore cancellation signals and continue with emitting elements

5. Conclusion

In this article, we discussed various ways of testing reactive streams from the Spring Reactor project.

First, we saw how to use StepVerifier to test publishers. Then, we saw how to use TestPublisher. Similarly, we saw how to operate with a misbehaving TestPublisher.

Както обикновено, изпълнението на всички наши примери може да бъде намерено в проекта Github.