Reaktive Streams mit StepVerifier und TestPublisher testen

Reaktive Streams mit StepVerifier und TestPublisher testen

1. Überblick

In diesem Tutorial werden wir uns das Testen vonreactive streams mitStepVerifier undTestPublisher genauer ansehen.

Wir werden unsere Untersuchung auf eineSpring Reactor-Anwendung stützen, die eine Kette von Reaktoroperationen enthält.

2. Maven-Abhängigkeiten

Spring Reactor wird mit mehreren Klassen zum Testen von reaktiven Strömen geliefert.

Wir können diese erhalten, indem wirthe reactor-test dependency hinzufügen:


    io.projectreactor
    reactor-test
    test
    3.2.3.RELEASE

3. StepVerifier

Im Allgemeinen hatreactor-test zwei Hauptverwendungen:

  • Erstellen eines schrittweisen Tests mitStepVerifier

  • Erstellen vordefinierter Daten mitTestPublisher , um nachgeschaltete Bediener zu testen

Der häufigste Fall beim Testen reaktiver Streams ist, wenn in unserem Code ein Publisher (aFlux orMono) definiert ist. We want to know how it behaves when someone subscribes. 

Mit derStepVerifier-API können wir unsere Erwartungen an veröffentlichte Elemente inwhat elements we expect and what happens when our stream completes definieren.

Zunächst erstellen wir einen Publisher mit einigen Operatoren.

Wir verwenden einFlux.just(T elements).. Diese Methode erstellt einFlux , das bestimmte Elemente ausgibt und dann abschließt.

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 unseresource mitStepVerifierin order to test what will happen when someone subscribes:

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

Zuerst erstellen wir einenStepVerifier builder mit dercreate method.

Als nächstes verpacken wir unsereFlux -Source, die getestet wird. Das erste Signal wird mitexpectNext(T element),  verifiziert, aber tatsächlich mitwe can pass any number of elements to expectNext.

Wir können auchexpectNextMatches and verwenden, umPredicate<T> für eine individuellere Übereinstimmung bereitzustellen.

Für unsere letzte Erwartung erwarten wir, dass unser Stream vollständig ist.

Und schließlichwe use verify() to trigger our test.

3.2. Ausnahmen inStepVerifier

Verketten wir nun den Herausgeber unseresFluxmitMono.

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

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

Nun, nach vier allen Elementen,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. Das SignalOnError benachrichtigt den Teilnehmer überthe publisher is closed with an error state. Therefore, we can’t add more expectations afterward.

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

  • expectError() – erwarten Fehler jeglicher Art

  • expectError(Class<? extends Throwable> clazz) – expect einen Fehler eines bestimmten Typs

  • expectErrorMessage(String errorMessage) – expect einen Fehler mit einer bestimmten Nachricht

  • expectErrorMatches(Predicate<Throwable> predicate) – erwarten einen Fehler, der einem bestimmten Prädikat entspricht

  • expectErrorSatisfies(Consumer<Throwable> assertionConsumer) – verbrauchen eineThrowable in-Anweisung, um eine benutzerdefinierte Zusicherung durchzuführen

3.3. Testen von zeitbasierten Publishern

Manchmal sind unsere Verlage zeitbasiert.

Angenommen, in unserer realen Anwendungwe have a one-day delay between events. 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 vonStepVerifier.withVirtualTimewurde entwickelt, um lang laufende Tests zu vermeiden.

We create a builder by calling withVirtualTime.Note that this method doesn’t take Flux as input. Stattdessen wirdSupplier benötigt, wodurch träge eine Instanz des getestetenFlux erstellt wird, nachdem der Scheduler eingerichtet wurde.

Um zu demonstrieren, wie wir eine erwartete Verzögerung zwischen Ereignissen testen können, erstellen wir einFlux mit einem Intervall von einer Sekunde, das zwei Sekunden lang ausgeführt wird. 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();

Beachten Sie, dass wir vermeiden sollten, denFlux -Searlier im Code zu instanziieren und dann denSupplier diese Variable zurückgeben zu lassen. Stattdessenwe should always instantiate Flux inside the lambda.

Es gibt zwei wichtige Erwartungsmethoden, die sich mit der Zeit befassen:

  • thenAwait(Duration duration) – unterbricht die Auswertung der Schritte; Während dieser Zeit können neue Ereignisse auftreten

  • expectNoEvent(Duration duration) – chlägt fehl, wenn während derduration ein Ereignis auftritt; Die Sequenz wird mit einem bestimmtenduration bestanden

Bitte beachten Sie, dass das erste Signal das Abonnementereignis ist, alsoevery expectNoEvent(Duration duration) should be preceded with *expectSubscription()*.

3.4. Zusicherungen nach der Ausführung mitStepVerifier

Wie wir gesehen haben, ist es einfach, unsere Erwartungen Schritt für Schritt zu beschreiben.

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

Erstellen wir einen benutzerdefinierten 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);

Wir erwarten, dass es eine 2 ausgibt, aber eine 4 fallen lässt, da wir zuerstemitter.complete aufgerufen haben.

Überprüfen wir dieses Verhalten mitverifyThenAssertThat. . Diese Methode gibtStepVerifier.Assertions on zurück, und wir können unsere Aussagen hinzufügen:

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

4. Daten mitTestPublisher erzeugen

Manchmal benötigen wir spezielle Daten, um die ausgewählten Signale auszulösen.

Zum Beispiel können wir eine ganz bestimmte Situation haben, die wir testen möchten.

Alternativ können wir unseren eigenen Operator implementieren und testen, wie er sich verhält.

In beiden Fällen können wirTestPublisher<T> verwenden, wobeiallows us to programmatically trigger miscellaneous signals:

  • next(T value) odernext(T value, T rest) – enden ein oder mehrere Signale an Teilnehmer

  • emit(T value) – same asnext(T) but ruft danachcomplete() auf

  • complete() - Beendet eine Quelle mit dem Signalcomplete

  • error(Throwable tr) – bestimmt eine Quelle mit einem Fehler

  • flux() – praktische Methode zum Umwickeln vonTestPublisher inFlux

  • mono()  - gleich usflux() , aber umschließtMono

4.1. TestPublisher erstellen

Erstellen wir ein einfachesTestPublisher , das einige Signale ausgibt und dann mit einer Ausnahme endet:

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

4.2. TestPublisher in Aktion

Wie bereits erwähnt, möchten wir manchmala finely chosen signal that closely matches to a particular situation. auslösen

In diesem Fall ist es besonders wichtig, dass wir die Datenquelle vollständig beherrschen. Um dies zu erreichen, können wir uns wieder aufTestPublisher verlassen.

Erstellen wir zunächst eine Klasse, dieFlux<String> als Konstruktorparameter verwendet, um die OperationgetUpperCase() auszuführen:

class UppercaseConverter {
    private final Flux source;

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

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

Angenommen,UppercaseConverter ist unsere Klasse mit komplexer Logik und Operatoren, und wir müssen ganz bestimmte Daten vomsource -Spublisher bereitstellen.

Wir können dies leicht mitTestPublisher: erreichen

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 diesem Beispiel erstellen wir einen TestFlux -Sublublisher imUppercaseConverter -Sconstructor-Parameter. Dann gibt unserTestPublisher drei Elemente aus und vervollständigt.

4.3. Fehlverhalten (TestPublisher

Andererseitswe can create a misbehaving TestPublisher with the createNonCompliant factory method. Wir müssen dem Konstruktor einen Aufzählungswert vonTestPublisher.Violation. übergeben. Diese Werte geben an, welche Teile von Spezifikationen unser Herausgeber möglicherweise übersieht.

Schauen wir uns einTestPublisher an, das keinNullPointerException für dasnull-Element wirft:

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

Zusätzlich zuALLOW_NULL, können wir auchTestPublisher.Violation to verwenden:

  • REQUEST_OVERFLOW - Ermöglicht das Aufrufen vonnext() ohne Auslösen vonIllegalStateException, wenn nicht genügend Anforderungen vorhanden sind

  • CLEANUP_ON_TERMINATE – verhindert, dass ein Abschlusssignal mehrmals hintereinander gesendet wird

  • DEFER_CANCELLATION – ermöglicht es uns, Löschsignale zu ignorieren und mit dem Aussenden von Elementen fortzufahren

5. Fazit

In diesem Artikel werdenwe discussed various ways of testing reactive streams from the Spring Reactor project.

Zuerst haben wir gesehen, wieStepVerifier zum Testen von Publishern verwendet werden. Dann haben wir gesehen, wie manTestPublisher. verwendet. In ähnlicher Weise haben wir gesehen, wie man mitTestPublisher arbeitet, die sich schlecht benehmen.

Wie üblich finden Sie die Implementierung aller unserer Beispiele inGithub project.