Testing Reactive Code — StepVerifier, WebTestClient, BlockHound
Fast recall
Quick revision
Core answer
reactor-test) is the primary tool for verifying Mono / Flux sequences — chain expectations like expectNext, expectError, thenRequest, then call .verify(). `WebTestClient` is the reactive alternative to MockMvc — slice-level via @WebFluxTest or full-stack via @SpringBootTest(webEnvironment = RANDOM_PORT). Use MockWebServer to fake WebClient upstreams, Testcontainers for real R2DBC / Kafka, and enable BlockHound to catch accidental blocking on event-loop threads. Always end StepVerifier chains with .verify() — without it the test passes trivially because nothing subscribed.Reactive-specific testing tools, virtual time, slice vs integration patterns, and pitfalls
Calling .block() in tests; missing .verify(); not using virtual time for delays; mocking with default null returns
Discuss StepVerifier backpressure testing, Hooks.onOperatorDebug, BlockHound integration, TestPublisher, and Testcontainers reuse
Interview-ready guide
Interview answer
Reactive testing uses specialized tools because regular JUnit assertions don't wait for async signals.
Core tools
StepVerifier— primary forMono/Flux; chain expectations, end with.verify()/.verifyComplete()/.verify(Duration)TestPublisher— controllable input with explicitnext/error/completeWebTestClient— endpoint testing; slice mode@WebFluxTestor real serverRANDOM_PORT- MockWebServer (okhttp) — fake upstream
WebClientcalls; WireMock alternative - BlockHound — catches accidental blocking on non-blocking schedulers
- Testcontainers — real Postgres / Mongo / Kafka in Docker
- Spring Boot test slices —
@SpringBootTest,@DataR2dbcTest,@WebFluxTest
Test layers
- Unit — pure Reactor chains with
StepVerifier - Slice —
@WebFluxTestcontroller + mocked service - Integration —
@SpringBootTest+WebTestClient+ Testcontainers
StepVerifier patterns
- Basic —
expectNextMatches,expectComplete,verify(Duration) - Counts —
expectNextCount(N).verifyComplete() - Errors —
expectError(Type.class)orexpectErrorMatches(predicate) - Backpressure —
initialRequest(0),thenRequest(N),expectNextCount,thenCancel - Custom —
assertNext(block),thenConsumeWhile(predicate)
WebTestClient patterns
- Slice —
@WebFluxTest(Controller.class)+@MockitoBean Service client.get().uri().exchange().expectStatus().isOk().expectBody(Type).value(...)- Validation —
expectStatus().isBadRequest()plusProblemDetailbody - Streaming SSE —
returnResult(ParameterizedTypeReference)plusStepVerifieron body - Integration —
@SpringBootTest(webEnvironment = RANDOM_PORT)plus Testcontainers
Mocking upstream
- MockWebServer — enqueue responses, assert request count, body delay for timeouts
- WireMock — richer, heavier
BlockHound and debug hooks
BlockHound.install()in@BeforeAll- Whitelist legacy blocking with
allowBlockingCallsInside Hooks.onOperatorDebug()for readable stack traces (slow; test-only)
Common pitfalls
- Forgetting
.verify()— passes trivially .block()in tests — deadlock risk- No timeout on
verify()— tests hang CI @MockitoBeanreturns null — reactive throws; alwayswhen().thenReturn(Mono.just(...))- Reactive transactions need
R2dbcTransactionManager - Slow integration tests — reuse Testcontainers as singletons
- Context propagation — MDC and tracing need explicit
contextWrite
Pyramid for a reactive service
- 100 unit, 30 slice, 10 integration, 3 contract
Strategic answer
StepVerifierfor reactive chains;.verify()is mandatory- Virtual time for delay-based operators (separate question)
WebTestClientfor endpoints; slice fast, real server for integration- MockWebServer for upstream; Testcontainers for real DB / Kafka
- BlockHound catches accidental blocking
- Slice tests over full context where possible
Testing Reactive Code — StepVerifier, WebTestClient, BlockHound
Direct Answer
Reactive testing uses specialized tools because regular JUnit assertions don't wait for async signals. StepVerifier (from reactor-test) is the primary tool for verifying Mono / Flux sequences — chain expectations like expectNext, expectError, thenRequest, then call .verify(). WebTestClient is the reactive alternative to MockMvc — slice-level via @WebFluxTest or full-stack via @SpringBootTest(webEnvironment = RANDOM_PORT). Use MockWebServer to fake WebClient upstreams, Testcontainers for real R2DBC / Kafka, and enable BlockHound to catch accidental blocking on event-loop threads. Always end StepVerifier chains with .verify() — without it the test passes trivially because nothing subscribed.
Common Mistakes & Pitfalls
Calling .block() in tests; missing .verify(); not using virtual time for delays; mocking with default null returns
Check your understanding
Answer these without looking back at the article.
- How do you test time-based operators like delayElements with virtual time?
- What's the difference between @WebFluxTest and @SpringBootTest?
- How do you test that backpressure is respected in a Flux chain?
- Why is BlockHound useful and how do you whitelist allowed blocking?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.