Use MockMvc when your Spring MVC controller reads authentication from servlet security, and use WebTestClient when the endpoint is reactive or you want HTTP-style tests that can also run against a server. For most classic Spring Boot MVC apps, MockMvc is the sharper tool for checking @AuthenticationPrincipal, Principal, Authentication, roles, CSRF, and denied access paths.
TLDR: If your controller is built with Spring MVC, start with MockMvc and spring-security-test. For example, a team with 120 secured controller tests may keep 85% of them in MockMvc because setup is fast and failures point straight to the controller or filter chain. Use WebTestClient for WebFlux, live-server tests, or when you want the same client style across reactive and servlet tests. A simple case: testing GET /me with @AuthenticationPrincipal UserDetails user is usually one line with .with(user("alice")) in MockMvc.
Why Authentication Parameters Matter in Controller Tests
Many secured controllers do not just ask, “Is this user logged in?” They read identity data directly:
@AuthenticationPrincipalfor the current domain user orUserDetailsPrincipalfor a username or subjectAuthenticationfor authorities, credentials, or custom details- JWT claims from OAuth2 resource server authentication
That means a controller test must do more than hit a URL. It must place a realistic authentication object into the security context. If that object is wrong, your test can pass while production breaks. Annoying? Very.
MockMvc: The Default Pick for Spring MVC Security Tests
MockMvc tests the servlet stack without starting a real web server. It runs requests through Spring MVC and, if configured, through the Spring Security filter chain. This makes it ideal for controller tests where speed and precise failure messages matter.
A typical setup looks like this:
@WebMvcTest(ProfileController.class)
@AutoConfigureMockMvc
class ProfileControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void returnsCurrentUser() throws Exception {
mockMvc.perform(get("/me")
.with(user("alice").roles("USER")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("alice"));
}
}
That single .with(user("alice")) call creates an authenticated user and stores it where Spring Security expects it. If the controller receives Principal principal, the name is available. If it receives Authentication authentication, authorities are present. If it uses @AuthenticationPrincipal, Spring can resolve the principal.
MockMvc also gives you handy shortcuts:
@WithMockUser(username = "alice", roles = "ADMIN").with(user("bob").authorities(new SimpleGrantedAuthority("SCOPE_read"))).with(jwt().jwt(jwt -> jwt.claim("tenant", "acme"))).with(oauth2Login()).with(csrf())for POST, PUT, PATCH, and DELETE
The best part: MockMvc keeps the test close to the controller. You can mock services, verify status codes, and check model attributes or JSON. It is quick. A small controller test often runs in under 200 milliseconds after the Spring context is warm.
Where MockMvc Gets Awkward
MockMvc is not a full HTTP client. It does not prove that your app works over a real socket. It also belongs to the servlet world. If your app uses WebFlux controllers, handlers, or reactive security, MockMvc is the wrong fit.
It drives me crazy that some teams mix MVC and WebFlux test patterns just because both compile. The result is usually a test that looks clean but skips the part that matters. A reactive security context is not the same thing as a servlet request security context.
Another pain point is custom principals. If your controller expects @AuthenticationPrincipal CustomUser user, the default user("alice") principal may not match. You may need:
CustomUser customUser = new CustomUser("alice", "tenant-42");
mockMvc.perform(get("/me")
.with(authentication(
new UsernamePasswordAuthenticationToken(
customUser,
"password",
List.of(new SimpleGrantedAuthority("ROLE_USER"))
)
)))
.andExpect(status().isOk());
This is not hard. It is just easy to forget until the resolver returns null.
WebTestClient: Better for Reactive and End-to-End Style Tests
WebTestClient started as the test client for WebFlux, but it can also be used in several Spring test setups. Its style feels like a real HTTP client. You send a request, exchange it, then assert on the response.
webTestClient
.mutateWith(mockUser("alice").roles("USER"))
.get()
.uri("/me")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.username").isEqualTo("alice");
For WebFlux security, this is the normal path. The security context lives in Reactor, not in a servlet thread-local. WebTestClient works with that model.
It is also useful when you want tests that feel closer to production. With @SpringBootTest(webEnvironment = RANDOM_PORT), it can call a running app. That catches issues MockMvc may not see, such as codecs, real networking behavior, and some serialization surprises.
MockMvc vs WebTestClient: Practical Comparison
| Question | Best choice |
|---|---|
Testing Spring MVC controller with @AuthenticationPrincipal? |
MockMvc |
| Testing WebFlux endpoint with reactive security? | WebTestClient |
| Need fast slice tests with mocked services? | MockMvc |
| Need live-server behavior? | WebTestClient |
| Testing JWT claims in servlet app? | MockMvc, usually |
| Testing request and response like an HTTP client? | WebTestClient |
Testing Common Authentication Parameter Types
For a controller like this:
@GetMapping("/me")
Map<String, String> me(@AuthenticationPrincipal UserDetails user) {
return Map.of("username", user.getUsername());
}
MockMvc keeps it direct:
mockMvc.perform(get("/me").with(user("alice")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("alice"));
For Principal:
@GetMapping("/name")
String name(Principal principal) {
return principal.getName();
}
The same MockMvc user works. For Authentication, roles and authorities are visible too.
For JWT-based controllers, be specific. If production expects a claim named sub, email, or tenant, put it in the test token. Fake only what your controller uses, but fake it accurately.
mockMvc.perform(get("/tenant")
.with(jwt().jwt(jwt -> jwt
.subject("alice")
.claim("tenant", "acme"))))
.andExpect(status().isOk());
Do Not Skip the Security Filter Chain
A dangerous controller test calls the method but bypasses security. That can be fine for pure unit tests, but it does not prove access rules. If your test says an admin endpoint works, also test that a normal user gets 403 Forbidden.
- Unauthenticated request should return
401or redirect to login. - Authenticated user without authority should return
403. - Authenticated user with correct role should return
200. - State-changing request without CSRF should fail when CSRF is enabled.
A Sensible Testing Strategy
Use a layered approach. Keep most controller security tests small and fast with MockMvc in MVC apps. Add a smaller number of full application tests with WebTestClient or another HTTP client. For example, in a service with 60 secured endpoints, you might write 180 MockMvc cases for role and principal behavior, plus 15 live-server tests for login, JWT conversion, and serialization.
This split catches bugs without turning the build into a coffee break. Expect to waste time on bloated full-context tests if every tiny access rule starts a real server.
Final Recommendation
Choose MockMvc for Spring MVC controller authentication parameters. It is fast, mature, and deeply tied to servlet security testing. Choose WebTestClient for WebFlux, reactive authentication, and tests that should behave like real HTTP calls. The tool should match the stack. When it does, testing Spring Security feels less like guesswork and more like a clear safety net.