Tester des flux réactifs avec StepVerifier et TestPublisher

Test de flux réactifs avec StepVerifier et TestPublisher

1. Vue d'ensemble

Dans ce didacticiel, nous allons examiner de près le test dereactive streams avecStepVerifier etTestPublisher.

Nous baserons notre enquête sur une applicationSpring Reactor contenant une chaîne d'opérations de réacteur.

2. Dépendances Maven

Spring Reactor est livré avec plusieurs classes pour tester les flux réactifs.

Nous pouvons les obtenir en ajoutantthe reactor-test dependency:


    io.projectreactor
    reactor-test
    test
    3.2.3.RELEASE

3. StepVerifier

En général,reactor-test a deux utilisations principales:

  • création d'un test pas à pas avecStepVerifier

  • production de données prédéfinies avecTestPublisher pour tester les opérateurs en aval

Le cas le plus courant dans le test de flux réactifs est celui où nous avons un éditeur (aFlux orMono) défini dans notre code. We want to know how it behaves when someone subscribes. 

Avec l'APIStepVerifier, nous pouvons définir nos attentes des éléments publiés en termes dewhat elements we expect and what happens when our stream completes.

Tout d'abord, créons un éditeur avec certains opérateurs.

Nous allons utiliser unFlux.just(T elements). Cette méthode va créer unFlux t qui émet des éléments donnés puis se termine.

Étant donné que les opérateurs avancés n'entrent pas dans le cadre de cet article, nous allons simplement créer un éditeur simple qui affiche uniquement les noms à quatre lettres mappés en majuscules:

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

3.1. Scénario pas à pas

Maintenant, testons notresource withStepVerifierin order to test what will happen when someone subscribes:

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

Tout d'abord, nous créons un constructeurStepVerifier avec la méthodecreate .

Ensuite, nous enveloppons notreFlux source, qui est en cours de test. Le premier signal est vérifié avecexpectNext(T element),  mais vraiment,we can pass any number of elements to expectNext.

Nous pouvons également utiliserexpectNextMatches and pour fournir unPredicate<T>  pour une correspondance plus personnalisée.

Pour notre dernière attente, nous nous attendons à ce que notre flux se termine.

Et enfin,we use verify() to trigger our test.

3.2. Exceptions enStepVerifier

Maintenant, concaténons notre éditeurFlux avecMono.

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

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

Maintenant, après quatre tous les éléments,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. Le signalOnError notifie à l'abonné quethe publisher is closed with an error state. Therefore, we can’t add more expectations afterward.

S'il n'est pas nécessaire de vérifier le type et le message de l'exception à la fois, nous pouvons utiliser l'une des méthodes dédiées:

  • expectError() – s'attendre à tout type d'erreur

  • expectError(Class<? extends Throwable> clazz) – expecter une erreur d'un type spécifique

  • expectErrorMessage(String errorMessage) – expect une erreur ayant un message spécifique

  • expectErrorMatches(Predicate<Throwable> predicate) – s'attendre à une erreur qui correspond à un prédicat donné

  • expectErrorSatisfies(Consumer<Throwable> assertionConsumer) – consomme un ordreThrowable in pour faire une assertion personnalisée

3.3. Test des éditeurs basés sur le temps

Parfois, nos éditeurs sont basés sur le temps.

Par exemple, supposons que dans notre application réelle,we have a one-day delay between events. Maintenant, évidemment, nous ne voulons pas que nos tests s'exécutent pendant une journée entière pour vérifier le comportement attendu avec un tel retard.

Le générateur deStepVerifier.withVirtualTime est conçu pour éviter les tests de longue durée.

We create a builder by calling withVirtualTime.Note that this method doesn’t take Flux as input. Au lieu de cela, il prend unSupplier, qui crée paresseusement une instance duFlux testé après avoir configuré l'ordonnanceur.

Pour montrer comment nous pouvons tester un délai attendu entre les événements, créons un commutateurFlux avec un intervalle d'une seconde qui s'exécute pendant deux secondes. 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();

Notez que nous devrions éviter d'instancier le searlierFlux dans le code et ensuite d'avoir leSupplier  renvoyer cette variable. Au lieu de cela,we should always instantiate Flux inside the lambda.

Il existe deux principales méthodes d’attente qui traitent du temps:

  • thenAwait(Duration duration) – met en pause l'évaluation des étapes; de nouveaux événements peuvent survenir pendant cette période

  • expectNoEvent(Duration duration) –  échoue lorsqu'un événement apparaît pendant lesduration; la séquence passera avec unduration donné

Veuillez noter que le premier signal est l'événement d'abonnement, doncevery expectNoEvent(Duration duration) should be preceded with *expectSubscription()*.

3.4. Assertions post-exécution avecStepVerifier

Ainsi, comme nous l'avons vu, il est simple de décrire nos attentes étape par étape.

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

Créons un éditeur personnalisé. 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);

Nous nous attendons à ce qu'il émette un 2, mais abandonne un 4, puisque nous avons appeléemitter.complete en premier.

Donc, vérifions ce comportement en utilisantverifyThenAssertThat.  Cette méthode retourneStepVerifier.Assertions fils auquel nous pouvons ajouter nos assertions:

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

4. Produire des données avecTestPublisher

Parfois, nous pouvons avoir besoin de données spéciales pour déclencher les signaux choisis.

Par exemple, nous pouvons avoir une situation très particulière que nous voulons tester.

Alternativement, nous pouvons choisir d'implémenter notre propre opérateur et vouloir tester son comportement.

Dans les deux cas, on peut utiliserTestPublisher<T>, quiallows us to programmatically trigger miscellaneous signals:

  • next(T value) ounext(T value, T rest) –  envoie un ou plusieurs signaux aux abonnés

  • emit(T value) –  identique ànext(T)  mais invoquecomplete() par la suite

  • complete() - termine une source avec le signalcomplete

  • error(Throwable tr) – termine une source avec une erreur

  • flux() – méthode pratique pour envelopper unTestPublisher intoFlux

  • mono() – même usflux()  mais retourne à unMono

4.1. Créer unTestPublisher

Créons un simpleTestPublisher t qui émet quelques signaux puis se termine avec une exception:

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

4.2. TestPublisher en action

Comme nous l'avons mentionné précédemment, nous pouvons parfois vouloir déclenchera finely chosen signal that closely matches to a particular situation.

Maintenant, il est particulièrement important dans ce cas que nous ayons une maîtrise totale de la source des données. Pour y parvenir, on peut à nouveau s'appuyer surTestPublisher.

Commençons par créer une classe qui utiliseFlux<String>  comme paramètre de constructeur pour effectuer l’opérationgetUpperCase():

class UppercaseConverter {
    private final Flux source;

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

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

Supposons queUppercaseConverter is notre classe avec une logique et des opérateurs complexes, et que nous devions fournir des données très particulières à partir du spublishersource .

Nous pouvons facilement y parvenir avecTestPublisher:

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();

Dans cet exemple, nous créons un spublisher de testFlux dans le paramètreUppercaseConverter constructor. Ensuite, notreTestPublisher émet trois éléments et se termine.

4.3. Mauvais comportementTestPublisher

D'autre part,we can create a misbehaving TestPublisher with the createNonCompliant factory method. Nous devons transmettre au constructeur une valeur d'énumération deTestPublisher.Violation. Ces valeurs spécifient quelles parties des spécifications notre éditeur peut ignorer.

Jetons un œil à unTestPublisher tqui ne lancera pas deNullPointerException  pour l'élémentnull:

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

En plus deALLOW_NULL,, nous pouvons également utiliserTestPublisher.Violation to:

  • REQUEST_OVERFLOW - permet d'appelernext() w sans lancer unIllegalStateException lorsque le nombre de requêtes est insuffisant

  • CLEANUP_ON_TERMINATE – autorise l'envoi de tout signal de terminaison plusieurs fois de suite

  • DEFER_CANCELLATION – nous permet d'ignorer les signaux d'annulation et de continuer avec les éléments émetteurs

5. Conclusion

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

Tout d'abord, nous avons vu comment utiliserStepVerifier pour tester les éditeurs. Ensuite, nous avons vu comment utiliserTestPublisher. De même, nous avons vu comment opérer avec unTestPublisher qui se comporte mal.

Comme d'habitude, l'implémentation de tous nos exemples se trouve dans lesGithub project.