Test des flux réactifs à l'aide de StepVerifier et TestPublisher

1. Vue d'ensemble

Dans ce didacticiel, nous allons examiner de près les tests de flux réactifs avec StepVerifier et TestPublisher .

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

2. Dépendances de Maven

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

Nous pouvons les obtenir en ajoutant la dépendance réacteur-test :

 io.projectreactor reactor-test test     3.2.3.RELEASE 

3. StepVerifier

En général, l' essai en réacteur a deux utilisations principales:

  • création d'un test étape par étape avec StepVerifier
  • production de données prédéfinies avec TestPublisher 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 (un Flux ou Mono ) défini dans notre code. Nous voulons savoir comment il se comporte lorsque quelqu'un s'abonne.

Avec l' API StepVerifier , nous pouvons définir nos attentes à l'égard des éléments publiés en fonction des éléments que nous attendons et de ce qui se passe lorsque notre flux se termine .

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

Nous utiliserons un Flux.just (éléments T). Cette méthode créera un flux qui émettra des éléments donnés, puis se terminera.

Étant donné que les opérateurs avancés sortent du cadre de cet article, nous allons simplement créer un éditeur simple qui ne génère que des noms de 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 notre source avec StepVerifier afin de tester ce qui se passera lorsque quelqu'un s'abonnera :

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

Tout d'abord, nous créons un générateur StepVerifier avec la méthode create .

Ensuite, nous enveloppons notre source Flux , qui est en cours de test. Le premier signal est vérifié avec expectNext (élément T), mais en réalité, nous pouvons passer n'importe quel nombre d'éléments à expectNext .

Nous pouvons également utiliser expectNextMatches et fournir un prédicat pour une correspondance plus personnalisée.

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

Et enfin, nous utilisons verify () pour déclencher notre test .

3.2. Exceptions dans StepVerifier

Maintenant, concaténons notre éditeur Flux avec Mono .

Nous allons faire résilier ce Mono immédiatement avec une erreur lors de l'inscription à :

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

Maintenant, après quatre éléments, nous nous attendons à ce que notre flux se termine avec une exception :

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

Nous ne pouvons utiliser qu'une seule méthode pour vérifier les exceptions. Le signal OnError informe l'abonné que l'éditeur est fermé avec un état d'erreur. Par conséquent, nous ne pouvons pas ajouter d'autres attentes par la suite .

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 () - attendez n'importe quel type d'erreur
  • expectError (Class clazz ) - attendez une erreur d'un type spécifique
  • expectErrorMessage (String errorMessage) - s'attendre à une erreur ayant un message spécifique
  • expectErrorMatches (Predicate predicate) - attendez une erreur qui correspond à un prédicat donné
  • expectErrorSatisfies (Consumer assertionConsumer) - consomme un Throwable afin de faire une assertion personnalisée

3.3. Test des éditeurs temporels

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

Par exemple, supposons que dans notre application réelle, nous ayons un délai d'un jour entre les événements . Maintenant, évidemment, nous ne voulons pas que nos tests fonctionnent pendant une journée entière pour vérifier le comportement attendu avec un tel retard.

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

Nous créons un générateur en appelant withVirtualTime . Notez que cette méthode ne prend pas Fluxcomme entrée. Au lieu de cela, il faut un fournisseur , qui crée paresseusement une instance du Flux testé après avoir configuré le planificateur.

Pour démontrer comment nous pouvons tester un délai attendu entre les événements, créons un flux avec un intervalle d'une seconde qui dure deux secondes. Si le minuteur fonctionne correctement, nous ne devrions obtenir que deux éléments:

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 devons éviter d'instancier le Flux plus tôt dans le code et que le Fournisseur retourne cette variable. Au lieu de cela, nous devrions toujours instancier Flux à l'intérieur du lambda.

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

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

Comme d'habitude, l'implémentation de tous nos exemples se trouve dans le projet Github.