skip to content

How do you test WebFlux endpoints and reactive services that depend on the SecurityContext?

level: principalimportance: should knowfreq 30%

answer

  1. spring-security-test dependency
  2. @WithMockUser + ReactorContextTestExecutionListener
  3. WebTestClient.mutateWith(mockUser()/mockAuthentication())
  4. mutateWith(csrf()) for POST/PUT/DELETE
  5. SecurityMockServerConfigurers (reactive, not MockMvc)

basics

~10 s

Use spring-security-test: annotate tests with @WithMockUser (a ReactorContext test listener injects the context), and for WebTestClient use client.mutateWith(mockUser()) / mockAuthentication() plus mutateWith(csrf()) for writes.

solid answer

~40 s

Add the `spring-security-test` dependency. For unit-testing a reactive service, `@WithMockUser` (or `@WithUserDetails`/a custom `@WithSecurityContext`) works even in reactive code because Spring Security registers a `ReactorContextTestExecutionListener` that writes the mock `SecurityContext` into the Reactor Context for the test's subscription — no ThreadLocal needed. For controller/integration tests with `WebTestClient`, prefer the mutators from `SecurityMockServerConfigurers`: `client.mutateWith(mockUser("alice").roles("ADMIN"))` or `mockAuthentication(auth)`, which inject the authentication into each exchange, and `mutateWith(csrf())` to satisfy CSRF on state-changing requests. You can also set the context manually in pure Reactor tests via `StepVerifier` with `.contextWrite(ReactiveSecurityContextHolder.withAuthentication(...))`. The key subtlety: because the context lives in the Reactor Context, tests must inject it into the *reactive* pipeline, not a ThreadLocal, or the mock user won't be seen.

code

java · 25 lines
java
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*;

@WebFluxTest(AdminController.class)
@Import(SecurityConfig.class)
class AdminControllerTest {

    @Autowired WebTestClient client;

    @Test
    void adminCanPost() {
        client
            .mutateWith(mockUser("alice").roles("ADMIN"))
            .mutateWith(csrf())                       // required: CSRF on for mutations
            .post().uri("/admin/things")
            .exchange()
            .expectStatus().isOk();
    }

    @Test
    void anonymousIsRejected() {
        client.get().uri("/admin/things")
            .exchange()
            .expectStatus().isUnauthorized();
    }
}

go deeper

for a junior

Know that @WithMockUser exists and spring-security-test is needed.

for a middle

Should use WebTestClient mutators and remember csrf() for writes.

for a senior

Should explain the ReactorContextTestExecutionListener and choose annotation vs mutator deliberately.

for a principal

Should design a test strategy across slices (service StepVerifier vs endpoint WebTestClient vs JWT resource-server) and articulate why context injection targets the Reactor Context.

## Dependency Everything below needs `org.springframework.security:spring-security-test` on the test classpath. ## 1. Annotation-driven (services and @WebFluxTest handlers) `@WithMockUser`, `@WithUserDetails`, and custom `@WithSecurityContext`/`@WithMockUser`-style annotations *do* work in reactive tests. Spring Security ships a **`ReactorContextTestExecutionListener`** (auto-registered) that takes the `SecurityContext` those annotations build and writes it into the **Reactor Context** for the duration of the test method. So a reactive service calling `ReactiveSecurityContextHolder.getContext()` sees the mock user. ```java @Test @WithMockUser(username = "alice", roles = "ADMIN") void returnsCurrentUser() { StepVerifier.create(service.currentUsername()) .expectNext("alice") .verifyComplete(); } ``` ## 2. WebTestClient mutators (integration) For `WebTestClient`, don't rely only on annotations — use the reactive configurers from **`SecurityMockServerConfigurers`** (static-imported), applied via `mutateWith`: ```java import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*; webTestClient .mutateWith(mockUser("alice").roles("ADMIN")) // inject authentication .mutateWith(csrf()) // satisfy CSRF for POST/PUT/DELETE .post().uri("/admin/things") .exchange() .expectStatus().isOk(); ``` Other configurers: `mockAuthentication(Authentication)`, `mockJwt()` (for a resource server), `mockOpaqueToken()`, `mockOidcLogin()`. These attach the credential into each exchange's Reactor Context so `authorizeExchange` rules and `@AuthenticationPrincipal` resolve correctly. `.mutateWith(...)` can be applied per-request or to a shared client via `webTestClient.mutate()`. ## 3. Pure Reactor tests Without Spring context you can set it directly: ```java StepVerifier.create( service.currentUsername() .contextWrite(ReactiveSecurityContextHolder.withAuthentication( new TestingAuthenticationToken("alice", "pw", "ROLE_ADMIN")))) .expectNext("alice") .verifyComplete(); ``` Remember bottom-up propagation: the `contextWrite` must be downstream of (after) the code reading the context. ## Gotchas that trip people - **CSRF:** WebFlux security enables CSRF by default for mutating methods; POST/PUT/DELETE tests fail with 403 unless you add `mutateWith(csrf())`. - **Wrong mutator source:** using MVC's `SecurityMockMvcRequestPostProcessors` (`user(...)`) with `WebTestClient` won't work; the reactive equivalents live in `SecurityMockServerConfigurers`. - **Annotation vs mutator:** `@WithMockUser` populates the *test thread's* reactive context via the listener; it's ideal for slice/service tests. For full `WebTestClient` request flows the mutator injects into the *exchange*, which is more faithful to a real request. When both are present, be deliberate — prefer mutators for `WebTestClient`. - **Roles vs authorities:** `mockUser("a").roles("ADMIN")` grants `ROLE_ADMIN`; `.authorities(...)` sets exact strings — match what your `hasRole`/`hasAuthority` rules expect. - **Blocking assertions:** don't `.block()` reactive results carelessly; use `StepVerifier` or `WebTestClient`'s fluent expectations. ## When to use which - Service unit test → `@WithMockUser` + `StepVerifier`, or manual `contextWrite`. - Endpoint/integration → `WebTestClient` + `SecurityMockServerConfigurers` mutators (+ `csrf()`). - Resource-server JWT → `mockJwt()`/`mockOpaqueToken()`.

  • Why does @WithMockUser work in a reactive test even though the context lives in the Reactor Context, not a ThreadLocal?
    spring-security-test registers a ReactorContextTestExecutionListener that writes the annotation-built SecurityContext into the Reactor Context for the test's subscription, so ReactiveSecurityContextHolder.getContext() resolves it.
  • A POST test with a valid mock user returns 403. What did you forget?
    WebFlux security enables CSRF by default for state-changing methods. Add .mutateWith(csrf()) so the request carries a valid CSRF token; the 403 is the CSRF filter rejecting the write.
  • Which class provides the WebTestClient security mutators, and how does it differ from the MVC equivalent?
    SecurityMockServerConfigurers (reactive) provides mockUser/mockAuthentication/mockJwt/csrf. The MVC equivalent, SecurityMockMvcRequestPostProcessors, is for MockMvc and does not work with WebTestClient.

saying these in an interview costs you the question

  • Claiming @WithMockUser cannot work in reactive tests
  • Using MockMvc's SecurityMockMvcRequestPostProcessors with WebTestClient
  • Forgetting mutateWith(csrf()) on POST/PUT/DELETE and blaming the mock user
  • Trying to set a ThreadLocal SecurityContextHolder in a reactive test and expecting the pipeline to see it

context