Have you ever added Spring Security to a UserController under test, only to watch every @WebMvcTest test start failing with 401 or 403? Even if you’re comfortable with the basics of using MockMvc, figuring out how to set up an authenticated state is a different story — the information out there tends to be scattered and confusing.
This article covers how to reliably pass Controller tests that require authentication, using @WithMockUser, @WithUserDetails, and SecurityMockMvcRequestPostProcessors as appropriate.
Why does @WebMvcTest return 401/403?
When spring-security-test and spring-boot-starter-security are on the classpath, @WebMvcTest boots the Controller layer’s context along with the security auto-configuration. This means the same filter chain used in production also applies to your tests, and according to your authorizeHttpRequests configuration, unauthenticated requests get rejected with 401 (or 302).
Mocking the Service with @MockBean (or @MockitoBean) doesn’t solve this, since that operates at a different layer from authentication. If you’re confused because “the Service is mocked, but I’m still getting a 401,” start by ruling out a security-side issue.
Reproducing an authenticated user with @WithMockUser
The easiest approach is @WithMockUser. Just add it to a test method (or class), and the request will run with a mock Authentication set in the SecurityContext.
@Test
@WithMockUser
void 認証済みなら200を返す() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk());
}
The default username is "user", and the default role is "ROLE_USER". To change the username or role, specify the attributes.
@Test
@WithMockUser(username = "admin", roles = {"ADMIN", "USER"})
void 管理者は管理画面にアクセスできる() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isOk());
}
One thing to watch out for here is the difference between roles() and authorities(). roles("ADMIN") is automatically treated as "ROLE_ADMIN", but authorities("ADMIN") is used as-is, as "ADMIN".
@Test
@WithMockUser(authorities = "SCOPE_read")
void authoritiesはROLE_プレフィックスを付与しない() throws Exception {
mockMvc.perform(get("/api/reports"))
.andExpect(status().isOk());
}
hasRole("ADMIN") internally converts the value to "ROLE_ADMIN" before comparing, so it pairs naturally with roles(). On the other hand, if you want to compare an arbitrary authority string directly — as with hasAuthority("SCOPE_read") — use authorities(). Mixing these up leads to the classic trap of “the role should be correct, but I’m still getting a 403,” so be sure to check this against the hasRole / hasAuthority settings on the SecurityConfig side.
Difference from @WithUserDetails
@WithMockUser creates a fully mocked user that’s self-contained within the test, but if you want to verify behavior using a principal actually loaded from your UserDetailsService implementation, @WithUserDetails is a better fit.
@Test
@WithUserDetails(value = "[email protected]", userDetailsServiceBeanName = "userDetailsService")
void 実際のUserDetailsServiceからロードしたユーザーで検証する() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("[email protected]"));
}
If your authorization data is defined in the DB or domain objects, and you want your tests to verify fields on a custom UserDetails implementation (such as an employee ID or tenant ID), choose this approach. Conversely, if you just need an authenticated state and nothing more, @WithMockUser is enough.
Flexibly injecting authentication with SecurityMockMvcRequestPostProcessors
Annotations can only be switched per test method. If you want to try different authentication states per request within the same test class, it’s convenient to pass user() or authentication() from SecurityMockMvcRequestPostProcessors into .with().
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@Test
void リクエスト単位でユーザーを切り替える() throws Exception {
mockMvc.perform(get("/api/orders/1").with(user("hanako").roles("USER")))
.andExpect(status().isOk());
mockMvc.perform(get("/api/orders/1").with(user("admin").roles("ADMIN")))
.andExpect(status().isOk());
}
If you want to pass an arbitrary Authentication object directly, use authentication().
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
Authentication auth = new UsernamePasswordAuthenticationToken(
"taro", null, List.of(new SimpleGrantedAuthority("ROLE_USER")));
mockMvc.perform(get("/api/orders/1").with(authentication(auth)))
.andExpect(status().isOk());
If you’re using JWTs in an OAuth2 resource server configuration, the same mechanism provides a jwt() postprocessor. JWT-specific configuration is out of scope for this article, so we’ll just mention that it exists.
POST/PUT/DELETE and CSRF tokens
With CSRF protection enabled, a 403 is returned for any non-GET request that doesn’t include a CSRF token. If authentication is succeeding but only POST requests return 403, this is often the cause.
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
@Test
@WithMockUser
void CSRFトークン付きなら注文を作成できる() throws Exception {
mockMvc.perform(post("/api/orders")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{\"itemId\": 1, \"quantity\": 2}"))
.andExpect(status().isCreated());
}
@Test
@WithMockUser
void CSRFトークンが無いと403になる() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isForbidden());
}
If your API is fully stateless (token-based authentication only) and CSRF protection itself is disabled by design, .with(csrf()) isn’t necessary. However, if you disable CSRF only on the test side, your tests will diverge from the actual production filter configuration — so make sure your tests match the actual csrf() settings in your SecurityConfig.
Verifying @PreAuthorize with @WebMvcTest
Method security with @PreAuthorize only works within a context where @EnableMethodSecurity is enabled. Since @WebMvcTest configures its context around a narrow set of Controller-related Beans, @PreAuthorize checks may be silently skipped unless you explicitly include SecurityConfig in the scan.
@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)
class OrderControllerSecurityTest {
@Autowired
MockMvc mockMvc;
@MockBean
OrderService orderService;
@Test
@WithMockUser(roles = "USER")
void USERロールでは削除できず403になる() throws Exception {
mockMvc.perform(delete("/api/orders/1").with(csrf()))
.andExpect(status().isForbidden());
}
}
By explicitly importing your authorization configuration with @Import(SecurityConfig.class), you can cover @PreAuthorize checks within the scope of a slice test. If you need to go further into more complex authorization logic (such as SpEL-based ownership checks), consider breaking that out into an integration test with @SpringBootTest.
Summary
For Controller tests that require authentication, you won’t have to guess if you pick the right tool for the situation.
- If you just need to reproduce an authenticated state, use
@WithMockUser - If you need verification that depends on an actual
UserDetailsServiceimplementation, use@WithUserDetails - If you need to switch authentication info per request, use
user()/authentication()fromSecurityMockMvcRequestPostProcessors - For POST/PUT/DELETE, always check whether
.with(csrf())is needed - If you also want to verify
@PreAuthorize, don’t forget@Import(SecurityConfig.class)
For the basics of @WebMvcTest itself and how to use @MockBean, see the basic guide to Controller testing; for a general approach to testing, see Introduction to testing with JUnit and Mockito.
Related Articles
- How to implement method security with @PreAuthorize in Spring Boot - This article covers implementing role-based authorization logic itself. Reading it alongside this testing approach will deepen your understanding.
- Spring Boot test annotations cheat sheet - Useful for an overview of test annotations beyond
@WithMockUser.