Testen reaktiver Streams mit StepVerifier und TestPublisher

1. Übersicht

In diesem Tutorial werden wir uns das Testen reaktiver Streams mit StepVerifier und TestPublisher genauer ansehen .

Wir werden unsere Untersuchung auf einer Basis Frühling Reactor - Anwendung enthält eine Kette von Reaktorbetrieb.

2. Maven-Abhängigkeiten

Der Federreaktor wird mit mehreren Klassen zum Testen reaktiver Ströme geliefert.

Wir können diese erhalten, indem wir die Reaktortestabhängigkeit hinzufügen :

 io.projectreactor reactor-test test     3.2.3.RELEASE 

3. StepVerifier

Im Allgemeinen hat der Reaktortest zwei Hauptanwendungen:

  • Erstellen eines schrittweisen Tests mit StepVerifier
  • Erstellen vordefinierter Daten mit TestPublisher zum Testen nachgeschalteter Bediener

Der häufigste Fall beim Testen reaktiver Streams ist, wenn in unserem Code ein Publisher (ein Flux oder Mono ) definiert ist. Wir möchten wissen, wie es sich verhält, wenn sich jemand anmeldet.

Mit der StepVerifier- API können wir unsere Erwartungen an veröffentlichte Elemente dahingehend definieren, welche Elemente wir erwarten und was passiert, wenn unser Stream abgeschlossen ist .

Lassen Sie uns zunächst einen Publisher mit einigen Operatoren erstellen.

Wir werden ein Flux.just (T-Elemente) verwenden. Diese Methode erstellt einen Fluss , der bestimmte Elemente ausgibt und dann vervollständigt.

Da erweiterte Operatoren den Rahmen dieses Artikels sprengen, erstellen wir lediglich einen einfachen Herausgeber, der nur Namen mit vier Buchstaben ausgibt, die Großbuchstaben zugeordnet sind:

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

3.1. Schritt-für-Schritt-Szenario

Testen wir nun unsere Quelle mit StepVerifier, um zu testen, was passiert, wenn jemand abonniert :

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

Zuerst erstellen wir einen StepVerifier- Builder mit der create- Methode.

Als nächstes wickeln wir unsere Flux- Quelle ein, die gerade getestet wird. Das erste Signal wird mit expectedNext (T-Element) verifiziert , aber wir können wirklich eine beliebige Anzahl von Elementen an expectedNext übergeben .

Wir können auch expectedNextMatches verwenden und ein Prädikat für eine benutzerdefinierte Übereinstimmung bereitstellen .

Für unsere letzte Erwartung erwarten wir, dass unser Stream abgeschlossen ist.

Und schließlich verwenden wir verify () , um unseren Test auszulösen .

3.2. Ausnahmen in StepVerifier

Lassen Sie uns nun unseren Flux- Publisher mit Mono verketten .

Dieses Mono wird sofort mit einem Fehler beendet, wenn es abonniert wird :

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

Nach vier Elementen erwarten wir nun, dass unser Stream mit einer Ausnahme beendet wird :

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

Wir können nur eine Methode verwenden, um Ausnahmen zu überprüfen. Das OnError- Signal benachrichtigt den Abonnenten, dass der Herausgeber mit einem Fehlerstatus geschlossen ist. Daher können wir danach keine weiteren Erwartungen hinzufügen .

Wenn es nicht erforderlich ist, den Typ und die Meldung der Ausnahme gleichzeitig zu überprüfen, können wir eine der dedizierten Methoden verwenden:

  • awaitError () - Erwarten Sie jede Art von Fehler
  • awaitError (Class clazz ) - Erwarten Sie einen Fehler eines bestimmten Typs
  • expectedErrorMessage (String errorMessage) - Erwarten Sie einen Fehler mit einer bestimmten Nachricht
  • expectedErrorMatches (Prädikat-Prädikat) - Erwarten Sie einen Fehler, der mit einem bestimmten Prädikat übereinstimmt
  • requireErrorSatisfies (Consumer assertionConsumer) - verbraucht ein Throwable , um eine benutzerdefinierte Assertion durchzuführen

3.3. Testen von zeitbasierten Publishern

Manchmal sind unsere Verlage zeitbasiert.

Angenommen, wir haben in unserer realen Anwendung eine Verzögerung von einem Tag zwischen den Ereignissen . Jetzt möchten wir natürlich nicht, dass unsere Tests einen ganzen Tag lang ausgeführt werden, um das erwartete Verhalten mit einer solchen Verzögerung zu überprüfen.

Der Builder StepVerifier.withVirtualTime wurde entwickelt, um lang laufende Tests zu vermeiden.

Wir erstellen einen Builder, indem wir withVirtualTime aufrufen . Beachten Sie, dass diese Methode kein Flux verwendetals Eingabe. Stattdessen wird ein Lieferant benötigt , der nach dem Einrichten des Schedulers träge eine Instanz des getesteten Flux erstellt .

Um zu demonstrieren, wie wir auf eine erwartete Verzögerung zwischen Ereignissen testen können, erstellen wir einen Fluss mit einem Intervall von einer Sekunde, der zwei Sekunden lang ausgeführt wird. Wenn der Timer korrekt läuft, sollten wir nur zwei Elemente erhalten:

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

Beachten Sie, dass wir vermeiden sollten, den Flux früher im Code zu instanziieren und dann vom Lieferanten diese Variable zurückgeben zu lassen. Stattdessen sollten wir immer Flux innerhalb des Lambda instanziieren .

Es gibt zwei Haupterwartungsmethoden, die sich mit der Zeit befassen:

  • 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.

Die Implementierung aller unserer Beispiele finden Sie wie gewohnt im Github-Projekt.