How is WebTestClient bound in a @WebFluxTest, and how do you handle Spring Security in the slice?
answer
- bindToApplicationContext = in-memory, no port
- Security filter chain runs in the slice → 401/403
- mutateWith(mockUser()/mockJwt()/csrf())
- @WithMockUser alternative
- bindToServer + RANDOM_PORT for real transport
basics
~20 sIn @WebFluxTest, WebTestClient binds directly to the in-memory WebFlux handler (no server/port), so tests are fast and deterministic. If Spring Security is on the classpath it also applies in the slice, so you use test security config or mutators like mockUser() to authenticate requests.
solid answer
~40 s@WebFluxTest auto-configures a WebTestClient bound to the ApplicationContext's WebFlux infrastructure (bindToApplicationContext) — an in-memory pipeline through DispatcherHandler, no real socket. That's why it's fast and free of network flakiness, but also why filters, converters and security run exactly as configured. If Spring Security WebFlux is present, its SecurityWebFilterChain is loaded in the slice, so unauthenticated calls get 401/403. To authenticate you either import your security config and use the reactive security test support — `WebTestClient.mutateWith(SecurityMockServerConfigurers.mockUser(...))` / `mockJwt()` from `spring-security-test` — or annotate with `@WithMockUser`. You can also mutate the client per request for headers, CSRF, or base URL. For true end-to-end you'd instead run a real server via @SpringBootTest(webEnvironment=RANDOM_PORT) and `WebTestClient.bindToServer().baseUrl(...)`, but that's outside the slice.
code
java · 22 lines@WebFluxTest(AdminController.class)
@Import(SecurityConfig.class) // load the SecurityWebFilterChain
class AdminControllerTest {
@Autowired WebTestClient webTestClient;
@Test
void unauthenticatedIsForbidden() {
webTestClient.get().uri("/admin/stats")
.exchange()
.expectStatus().isUnauthorized();
}
@Test
void adminCanRead() {
webTestClient
.mutateWith(SecurityMockServerConfigurers.mockUser("root").roles("ADMIN"))
.get().uri("/admin/stats")
.exchange()
.expectStatus().isOk();
}
}go deeper
Know WebTestClient runs in-memory and that secured endpoints may need a mock user.
Apply mutateWith(mockUser/csrf) and import security config appropriately.
Explain binding modes, security filter execution, streaming assertions with StepVerifier, and timeouts.
Decide slice vs full-server tests based on what transport/security surface must be covered, and standardize the team's reactive test-security approach.
## How WebTestClient is bound `WebTestClient` supports several bindings: - **`bindToApplicationContext(ctx)`** — wires the client to the WebFlux `DispatcherHandler`/`WebHandler` chain from the context, **in memory, no HTTP server, no port**. This is what `@WebFluxTest` uses. Requests flow through the real WebFilter chain, codecs, and handler mapping but never hit a socket. - **`bindToController(...)` / `bindToRouterFunction(...)`** — standalone, minimal setup around a single controller or functional route. - **`bindToServer().baseUrl("http://localhost:8080")`** — a real network client against a running server (used with `@SpringBootTest(webEnvironment = RANDOM_PORT)`), for full end-to-end. The in-memory binding is why slice tests are **fast and deterministic**: no ephemeral port, no connection setup. But it also means everything configured in the web layer — **WebFilters, exception handlers, content negotiation, and Spring Security** — executes just as in production. ## Security in the slice Key gotcha: if **Spring Security (WebFlux)** is on the classpath, `@WebFluxTest` **loads the security configuration** (the `SecurityWebFilterChain`). Requests are therefore subject to authentication/authorization. Unauthenticated calls to protected endpoints return **401/403**, surprising people who expected an open endpoint. Options: 1. **`spring-security-test` mutators** — the reactive way: `webTestClient.mutateWith(SecurityMockServerConfigurers.mockUser("alice").roles("ADMIN"))` or `.mutateWith(mockJwt())`, `.mutateWith(csrf())`. `mutateWith` returns a configured client that injects the security context for the request. 2. **`@WithMockUser` / `@WithMockJwt`** annotations set up a mock `SecurityContext` for the test method. 3. **Import a permissive test security config** with `@Import(TestSecurityConfig.class)` if you want to bypass auth entirely for controller-focused tests. CSRF: state-changing requests (POST/PUT/DELETE) may need `.mutateWith(csrf())` when CSRF protection is enabled. ## Per-request and per-client mutation `WebTestClient` is immutable; `.mutate()` returns a builder to tweak timeouts, base URL, default headers; `.mutateWith(configurer)` applies a `WebTestClientConfigurer` (how security mutators plug in). You can also set headers inline: `.get().uri(...).headers(h -> h.setBearerAuth(token))`. ## Reactive assertions Handlers return `Mono`/`Flux`; `WebTestClient` subscribes and materializes, so `.expectBodyList(Foo.class).hasSize(3)` works. For streaming (`text/event-stream`) you can use `returnResult()` + `StepVerifier` on the `Flux` body to assert element-by-element. ## Gotchas - **Security applied unexpectedly** → 401/403 in slice; add mutators or test config. - **Response timeout**: default WebTestClient response timeout is 5s; long reactive pipelines may need `.mutate().responseTimeout(Duration.ofSeconds(30))`. - **In-memory binding ≠ real server**: connection-level concerns (TLS, real headers set by the container, port) aren't exercised — use `bindToServer` for those. - **Wrong slice**: security beans your controller needs (e.g., a custom `ReactiveUserDetailsService`) may be missing and require mocking/import. ## When to use which binding Use the default in-memory slice binding for controller logic, validation, serialization, error mapping, and authz rules. Switch to `bindToServer` with a running context only when you must exercise the real transport (filters added by the server, TLS, actual ports).
- Why does a protected endpoint return 401 in @WebFluxTest even though your integration test passes?Because @WebFluxTest loads the SecurityWebFilterChain and applies it in-memory; without an authenticated context the request is rejected. Use spring-security-test mutators (mockUser/mockJwt) or @WithMockUser to authenticate the request.
- How would you test a text/event-stream (Flux) endpoint element by element?Call .exchange().returnResult(Foo.class) to get a FluxExchangeResult, take getResponseBody() (a Flux<Foo>), and assert with StepVerifier: create the verifier, expectNext each element, then verifyComplete().
saying these in an interview costs you the question
- Claiming WebTestClient always opens a real port in @WebFluxTest
- Assuming security is bypassed in the slice
- Using MockMvc-style @WithMockUser without knowing reactive mutators exist
- Not knowing the default 5s WebTestClient response timeout can fail slow pipelines