Testando fluxos reativos usando StepVerifier e TestPublisher

Testando fluxos reativos usando StepVerifier e TestPublisher

1. Visão geral

Neste tutorial, daremos uma olhada de perto no teste dereactive streams comStepVerifiereTestPublisher.

Basearemos nossa investigação em um aplicativoSpring Reactor contendo uma cadeia de operações do reator.

2. Dependências do Maven

O Spring Reactor vem com várias classes para testar fluxos reativos.

Podemos obtê-los adicionandothe reactor-test dependency:


    io.projectreactor
    reactor-test
    test
    3.2.3.RELEASE

3. StepVerifier

Em geral,reactor-test tem dois usos principais:

  • criando um teste passo a passo comStepVerifier

  • produção de dados predefinidos comTestPublisher to teste operadores downstream

O caso mais comum em testar fluxos reativos é quando temos um editor (aFlux orMono) definido em nosso código. We want to know how it behaves when someone subscribes. 

Com a APIStepVerifier, podemos definir nossas expectativas de elementos publicados em termos dewhat elements we expect and what happens when our stream completes.

Em primeiro lugar, vamos criar um editor com alguns operadores.

Usaremos umFlux.just(T elements). Este método criará umFlux que emite determinados elementos e, em seguida, conclua.

Como os operadores avançados estão além do escopo deste artigo, criaremos apenas um editor simples que produza apenas nomes de quatro letras mapeados em maiúsculas:

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

3.1. Cenário passo a passo

Agora, vamos testar nossosource 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();

Primeiro, criamos umStepVerifier builder com o métodocreate .

Em seguida, envolvemos nossa fonteFlux , que está sendo testada. O primeiro sinal é verificado comexpectNext(T element), , mas realmente,we can pass any number of elements to expectNext.

Também podemos usarexpectNextMatches e fornecer umPredicate<T>  para uma correspondência mais personalizada.

Para nossa última expectativa, esperamos que nosso fluxo seja concluído.

E finalmente,we use verify() to trigger our test.

3.2. Exceções emStepVerifier

Agora, vamos concatenar nosso editorFlux comMono.

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

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

Agora, depois de quatro todos os elementos,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. O sinalOnError notifica o assinante quethe publisher is closed with an error state. Therefore, we can’t add more expectations afterward.

Se não for necessário verificar o tipo e a mensagem da exceção de uma vez, podemos usar um dos métodos dedicados:

  • expectError() – espera qualquer tipo de erro

  • expectError(Class<? extends Throwable> clazz) – expect um erro de um tipo específico

  • expectErrorMessage(String errorMessage) – expect um erro com uma mensagem específica

  • expectErrorMatches(Predicate<Throwable> predicate) – espera um erro que corresponda a um determinado predicado

  • expectErrorSatisfies(Consumer<Throwable> assertionConsumer) – consumir uma ordem de pecadoThrowable para fazer uma declaração personalizada

3.3. Testando editores com base no tempo

Às vezes, nossos editores são baseados no tempo.

Por exemplo, suponha que em nosso aplicativo da vida real,we have a one-day delay between events. Agora, obviamente, não queremos que nossos testes sejam executados por um dia inteiro para verificar o comportamento esperado com tanto atraso.

O construtorStepVerifier.withVirtualTime foi projetado para evitar testes de longa execução.

We create a builder by calling withVirtualTime.Note that this method doesn’t take Flux as input. Em vez disso, leva umSupplier, que preguiçosamente cria uma instância doFlux testado mais seguro tendo o agendador configurado.

Para demonstrar como podemos testar um atraso esperado entre eventos, vamos criar umFlux com um intervalo de um segundo que é executado por dois segundos. 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();

Observe que devemos evitar instanciarFlux mais cedo no código e, em seguida, fazer com queSupplier retorne essa variável. Em vez disso,we should always instantiate Flux inside the lambda.

Existem dois métodos principais de expectativa que lidam com o tempo:

  • thenAwait(Duration duration) – pausa a avaliação das etapas; novos eventos podem ocorrer durante este tempo

  • expectNoEvent(Duration duration) – falha quando qualquer evento aparece duranteduration; a sequência passará com um determinadoduration

Observe que o primeiro sinal é o evento de inscrição, entãoevery expectNoEvent(Duration duration) should be preceded with *expectSubscription()*.

3.4. Asserções pós-execução comStepVerifier

Então, como vimos, é simples descrever nossas expectativas passo a passo.

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

Vamos criar um editor personalizado. 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);

Esperamos que ele emita um 2, mas deixe cair 4, pois chamamosemitter.complete primeiro.

Então, vamos verificar esse comportamento usandoverifyThenAssertThat. . Este método retornaStepVerifier.Assertions on, ao qual podemos adicionar nossas asserções:

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

4. Produzindo dados comTestPublisher

Às vezes, podemos precisar de alguns dados especiais para acionar os sinais escolhidos.

Por exemplo, podemos ter uma situação muito particular que queremos testar.

Como alternativa, podemos optar por implementar nosso próprio operador e desejar testar como ele se comporta.

Para ambos os casos, podemos usarTestPublisher<T>, queallows us to programmatically trigger miscellaneous signals:

  • next(T value) ounext(T value, T rest) – senviar um ou mais sinais para assinantes

  • emit(T value) – same comonext(T)  mas invocacomplete() depois

  • complete() - termina uma fonte com o sinalcomplete

  • error(Throwable tr) – estermina uma fonte com um erro

  • flux() – método conveniente para envolver umTestPublisher intoFlux

  • mono() – mesmo usflux()  mas quebra para umMono

4.1. Criando umTestPublisher

Vamos criar umTestPublisher imples que emite alguns sinais e termina com uma exceção:

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

4.2. TestPublisher em ação

Como mencionamos anteriormente, às vezes podemos querer acionara finely chosen signal that closely matches to a particular situation.

Agora, é especialmente importante neste caso que tenhamos domínio completo sobre a fonte dos dados. Para conseguir isso, podemos novamente contar comTestPublisher.

Primeiro, vamos criar uma classe que usaFlux<String> as como o parâmetro do construtor para realizar a operaçãogetUpperCase():

class UppercaseConverter {
    private final Flux source;

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

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

Suponha queUppercaseConverter  é nossa classe com lógica e operadores complexos, e precisamos fornecer dados muito específicos do spublishersource .

Podemos facilmente conseguir isso comTestPublisher:

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

Neste exemplo, criamos um testeFlux publisher no parâmetroUppercaseConverter constructor. Então, nossoTestPublisher emite três elementos e completa.

4.3. Comportamento incorretoTestPublisher

Por outro lado,we can create a misbehaving TestPublisher with the createNonCompliant factory method. Precisamos passar no construtor um valor enum deTestPublisher.Violation. Esses valores especificam quais partes das especificações nosso editor pode ignorar.

Vamos dar uma olhada em umTestPublisher t que não lançará umNullPointerException  para o elementonull:

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

Além deALLOW_NULL,, também podemos usarTestPublisher.Violation to:

  • REQUEST_OVERFLOW - permite chamarnext() em lançar umIllegalStateException quando há um número insuficiente de solicitações

  • CLEANUP_ON_TERMINATE – permite o envio de qualquer sinal de encerramento várias vezes seguidas

  • DEFER_CANCELLATION – nos permite ignorar os sinais de cancelamento e continuar emitindo elementos

5. Conclusão

Neste artigo,we discussed various ways of testing reactive streams from the Spring Reactor project.

Primeiro, vimos como usarStepVerifier para testar editores. Então, vimos como usarTestPublisher. Da mesma forma, vimos como operar com umTestPublisher que se comporta mal.

Como de costume, a implementação de todos os nossos exemplos pode ser encontrada emGithub project.