Testando código reativo — StepVerifier e @WebFluxTest
TL;DR
StepVerifieré o “JUnit do reativo”: ele assina oMono/Fluxe verifica o fluxo passo a passo —expectNext(...)para cada item,expectError(...)para a falha esperada, everifyComplete()/verify()no fim para realmente disparar a assinatura.expectNextCount(n)conta itens sem casar valor a valor.StepVerifier.withVirtualTime(() -> ...)testa delays (Mono.delay,Flux.interval) sem esperar o tempo real — o relógio é virtual ethenAwait(Duration)adianta o ponteiro. Para reactive controllers,@WebFluxTestsobe só a fatia WebFlux e injeta umWebTestClientque exercita os endpoints de forma não-bloqueante. Dependência:io.projectreactor:reactor-test(scopetest).
O que é
Código reativo não retorna valores — retorna publishers preguiçosos (Mono/Flux) que só executam quando alguém assina. Um teste comum (assertEquals(esperado, metodo())) não funciona: ele compararia o objeto Flux em si, não os itens que ele emite. Pior, sem assinatura nada roda.
StepVerifier (do módulo reactor-test) é a ferramenta canônica para esse cenário. Ele é o assinante de teste: descreve uma sequência de expectativas sobre o fluxo de sinais (onNext, onError, onComplete) e, ao chamar verify()/verifyComplete(), assina o publisher e valida cada sinal contra a expectativa correspondente — falhando o teste se algo divergir.
Do lado HTTP, @WebFluxTest é a fatia de teste do Spring Boot para reactive controllers: sobe apenas a infraestrutura WebFlux (@Controller, @ControllerAdvice, WebFluxConfigurer, filtros, conversores) e auto-configura um WebTestClient para bater nos endpoints sem subir servidor real nem o contexto inteiro.
Os tipos reativos em si (
Mono,Flux, backpressure, schedulers) e o stack WebFlux são do Galho 11 — aqui o foco é testá-los, não reexplicá-los. Veja Spring WebFlux e WebClient.
Por que importa
Reativo testado errado é reativo não testado. Os dois erros mais caros são silenciosos:
- Esquecer o
verify: a cadeia deexpectNextparece um teste, mas semverify()/verifyComplete()oStepVerifiernunca assina — o publisher nem executa, e o teste passa verde sem ter exercido uma linha de produção. É um falso positivo perfeito. block()no teste: bloquear oMono/Fluxpara “pegar o valor” funciona em casos triviais, mas joga fora a natureza assíncrona (ordem de emissão, sinais de erro, backpressure) e pode travar o teste se o fluxo nunca completar.
Além disso, código reativo frequentemente tem tempo embutido — timeouts, retries com backoff, Flux.interval, polling. Testar isso com tempo real torna a suíte lenta e flaky. withVirtualTime resolve: um delay de uma hora passa em microssegundos de relógio virtual. Em entrevista sênior, saber distinguir StepVerifier de block(), e saber por que verify() é obrigatório, é o tipo de detalhe que separa quem usou reativo de quem só leu sobre.
Como funciona
StepVerifier: o JUnit do reativo (expectNext / expectError / verifyComplete)
O fluxo básico é sempre o mesmo: create → expectativas → verify.
StepVerifier.create(publisher)envolve oMono/Fluxsob teste e abre a cadeia de expectativas.expectNext(a, b, c)afirma que os próximos sinaisonNextcarregam exatamente esses valores, nessa ordem.expectError(IllegalArgumentException.class)/expectErrorMessage("...")afirmam que a sequência termina com aquele erro.verifyComplete()afirma término poronCompletee dispara a verificação (assina o publisher e bloqueia até terminar ou falhar).verify()é a forma genérica, usada quando o término esperado é um erro.
A regra de ouro: a cadeia só executa no verify. Tudo antes é descrição declarativa; o verify/verifyComplete é o gatilho que assina e roda.
expectNextCount e asserções sobre o fluxo
Quando os valores exatos não importam — só a quantidade — expectNextCount(long) conta n sinais onNext sem casar valor a valor. Útil para fluxos grandes ou não-determinísticos onde casar item a item seria frágil.
Há um arsenal de variantes para asserções mais ricas:
expectNextMatches(predicate)— casa o próximo item contra um predicado (o -> o.total() > 0).assertNext(consumer)— entrega o próximo item a um consumidor onde você roda asserções AssertJ/JUnit livremente.expectNextSequence(iterable)— casa contra uma coleção esperada inteira.thenConsumeWhile(predicate)— drena itens enquanto o predicado for verdadeiro (útil antes de checar o término).
Combine: expectNext(primeiro).expectNextCount(2).expectNextMatches(...).verifyComplete().
withVirtualTime: testar delays sem esperar de verdade
Fluxos com tempo (Mono.delay, Flux.interval, timeout, retry com backoff) não podem ser testados com Thread.sleep — seria lento e flaky. StepVerifier.withVirtualTime(Supplier<Publisher>) instala um VirtualTimeScheduler e, crucialmente, recebe o publisher dentro de um Supplier (lambda) — porque ele precisa ser construído depois que o scheduler virtual está ativo, senão o agendamento usa o relógio real.
Com o relógio virtual ligado:
thenAwait(Duration.ofHours(1))adianta o ponteiro uma hora instantaneamente, em vez de bloquear.expectNoEvent(Duration)afirma que, durante aquela janela virtual, nada foi emitido (bom para validar que o delay realmente segura o item).
Um delay de horas vira um teste de milissegundos, e é determinístico.
@WebFluxTest + WebTestClient: testar reactive controllers
@WebFluxTest é a fatia de teste para controllers WebFlux. Diferente de @SpringBootTest, ela não sobe o contexto inteiro: limita os beans escaneados a @Controller, @ControllerAdvice, WebFluxConfigurer, Filter, conversores e afins — @Component/@Service comuns não são carregados. As dependências do controller (o OrderService, por exemplo) você fornece como mock via @MockitoBean.
Em troca, ela auto-configura um WebTestClient (injetado por @Autowired), um cliente reativo de teste com API fluente: .get().uri(...).exchange().expectStatus().isOk().expectBodyList(Order.class). Ele exercita o controller de ponta a ponta (roteamento, serialização, content negotiation) sem servidor HTTP real e sem bloquear threads. Restrinja o escopo com @WebFluxTest(OrderController.class) para subir só o controller sob teste.
Na prática
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import static org.mockito.BDDMockito.given;
class OrderReactiveTests {
// (1) Flux que emite 3 pedidos e completa — contagem, sem casar valor a valor
@Test
void findAll_emiteTresPedidosECompleta() {
OrderService orderService = // ... colaborador sob teste
StepVerifier.create(orderService.findAll())
.expectNextCount(3)
.verifyComplete();
}
// (2) Caminho de erro: a sequência deve terminar com a exceção esperada
@Test
void findById_inexistente_emiteErro() {
OrderService orderService = // ...
StepVerifier.create(orderService.findById("nope"))
.expectErrorMatches(e -> e instanceof OrderNotFoundException
&& e.getMessage().contains("nope"))
.verify();
}
// (3) Delay de 1 hora testado em microssegundos com relógio virtual
@Test
void delayedOrder_naoEsperaTempoReal() {
StepVerifier.withVirtualTime(() ->
Mono.delay(Duration.ofHours(1)).thenReturn(new Order("o-1")))
.expectSubscription()
.expectNoEvent(Duration.ofHours(1)) // nada durante a janela virtual
.thenAwait(Duration.ofHours(1)) // adianta o relógio 1h, instantâneo
.expectNextMatches(o -> o.id().equals("o-1"))
.verifyComplete();
}
}
// (4) Reactive controller via fatia WebFlux + WebTestClient
@WebFluxTest(OrderController.class)
class OrderControllerWebFluxTests {
@Autowired
private WebTestClient webClient;
@MockitoBean
private OrderService orderService; // colaborador mockado, não sobe o real
@Test
void getOrders_retorna200ELista() {
given(orderService.findAll())
.willReturn(Flux.just(new Order("o-1"), new Order("o-2")));
webClient.get().uri("/orders")
.exchange()
.expectStatus().isOk()
.expectBodyList(Order.class).hasSize(2);
}
}
Order,OrderServiceeCustomersão domínios neutros de exemplo.Mono/Flux/WebFlux vêm do Galho 11 — aqui só se testa o que eles produzem.
Armadilhas
(1) Usar block() no teste em vez de StepVerifier
Bloquear o publisher para “pegar o valor” parece prático, mas descarta tudo que é reativo: ordem dos sinais, propagação de erro, backpressure, e a própria assincronia. Se o fluxo nunca completar (um Flux infinito, um Mono que depende de um sinal que não chega), o teste trava até o timeout.
// ERRADO — bloqueia, perde a natureza reativa, pode travar
Order o = orderService.findById("o-1").block();
assertEquals("o-1", o.id());
// CERTO — assina como assinante de teste e verifica o fluxo
StepVerifier.create(orderService.findById("o-1"))
.expectNextMatches(found -> found.id().equals("o-1"))
.verifyComplete();Fix: trate Mono/Flux com StepVerifier. Reserve block() para main()/scripts, nunca para testes de fluxo.
(2) Esquecer verifyComplete()/verify() no fim
Sem o verify, o StepVerifier não assina o publisher — a cadeia de expectNext é só uma descrição que nunca é exercida. O código de produção não roda, e o teste passa verde sem ter testado nada. É o falso positivo mais perigoso do reativo.
// ERRADO — nenhum verify: o publisher NUNCA é assinado, nada executa, teste passa vazio
StepVerifier.create(orderService.findAll())
.expectNextCount(3); // <- cadeia montada e... abandonada
// CERTO — verifyComplete() dispara a assinatura e valida o término
StepVerifier.create(orderService.findAll())
.expectNextCount(3)
.verifyComplete();Fix: toda cadeia StepVerifier termina em verifyComplete() (término por onComplete) ou verify() (quando o esperado é erro). Sem isso, não há teste.
(3) Testar um delay com tempo real
Validar um Mono.delay(Duration.ofHours(1)) (ou retry com backoff, Flux.interval, timeout) com o relógio real é inviável — a suíte trava por uma hora, ou alguém mete um Thread.sleep que deixa o teste lento e flaky.
// ERRADO — relógio real: o teste literalmente espera 1 hora (ou trava no timeout)
StepVerifier.create(Mono.delay(Duration.ofHours(1)).thenReturn(new Order("o-1")))
.expectNextMatches(o -> o.id().equals("o-1"))
.verifyComplete();
// CERTO — relógio virtual: thenAwait adianta o tempo, teste roda em microssegundos
StepVerifier.withVirtualTime(() ->
Mono.delay(Duration.ofHours(1)).thenReturn(new Order("o-1")))
.expectSubscription()
.thenAwait(Duration.ofHours(1))
.expectNextMatches(o -> o.id().equals("o-1"))
.verifyComplete();Fix: use StepVerifier.withVirtualTime(() -> ...) — passe o publisher dentro do Supplier (lambda) para que seja construído já sob o VirtualTimeScheduler — e adiante o relógio com thenAwait(Duration).
Em entrevista
Frase pronta (inglês)
To test reactive code I use
StepVerifierfromreactor-test— it acts as a test subscriber that asserts the stream signal by signal:expectNextfor emitted items,expectErrorfor the terminal error, andverifyComplete(orverify) at the end, which is what actually subscribes and runs the publisher. A common pitfall is omitting theverifycall: without it the chain never subscribes, so the production code never executes and the test passes green having tested nothing. For time-based operators like delays, retries or intervals I switch toStepVerifier.withVirtualTime, which installs a virtual clock sothenAwait(Duration.ofHours(1))advances time instantly instead of blocking. For reactive controllers I rely on the@WebFluxTestslice, which boots only the WebFlux layer and auto-configures aWebTestClientto exercise endpoints non-blockingly, mocking collaborators with@MockitoBean.
Vocabulário
| Termo (EN) | Como usar |
|---|---|
| test subscriber | ”StepVerifier acts as a test subscriber that drives the publisher.” |
| signal by signal | ”It asserts the stream signal by signal — onNext, onError, onComplete.” |
| terminal signal | ”verifyComplete asserts the completion terminal signal.” |
| virtual time | ”withVirtualTime installs a virtual time scheduler to skip real delays.” |
| to advance the clock | ”thenAwait advances the clock instead of blocking the thread.” |
| sliced test | ”@WebFluxTest is a sliced test — only the WebFlux layer, not the full context.” |
| to mock collaborators | ”I mock collaborators like the service with @MockitoBean.” |
| false positive | ”Forgetting verify is a false positive — green but never subscribed.” |
Veja também
- Testando código assíncrono
- Testes de integração ponta a ponta
- Spring WebFlux
- WebClient
- Testes (MOC do galho)
- Trilha Java
- Dicionário de Java
Referências
- Reactor Reference — Testing (
StepVerifier,withVirtualTime,reactor-test): https://projectreactor.io/docs/core/release/reference/testing.html - Spring Boot — Testing applications (
@WebFluxTest,WebTestClient): https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html