CodeGym /Courses /Spring Security /@WithMockUser and roles in MockMvc

@WithMockUser and roles in MockMvc

Spring Security
Level 26, Lesson 3
Available

1. @WithMockUser and user(): when to use which

When you first see @WithMockUser, it seems like just “another magical annotation” that you could do without. In reality, its appearance is very practical: sometimes you want the test to read like a pure access rule, without noise. That is, you want only the endpoint and the expected status to remain in the code, not the “decorations” around the user.

Imagine that you are writing a series of short tests: “editor can”, “user cannot”, “anonymous cannot”. If in each test you constantly add .with(user("...").roles("...")), the code starts to look like a set of similar spells. And @WithMockUser lets you move “who exactly came” to the top, into the annotation, and leave only “what we are checking” in the test.

There is another important point: @WithMockUser is not a “login” and not a password test. It is more like a temporary conference pass. You do not go through the real registration desk; you just put on a badge with EDITOR, and security lets you into the right room. For a large number of authorization tests, that is exactly what you need, because in such tests we care not about the password, but about the access boundaries.

Finally, in a team, @WithMockUser is often perceived as a “standard short style” that is faster to read. If the goal is to build a compact regression pack, the annotation style can be very convenient.

2. The working model of @WithMockUser

@WithMockUser has one superpower and one super-caveat. The superpower is that it puts an Authentication object into the SecurityContext before the test method runs, as if the user were already authenticated. The super-caveat is that it does this without a real login: no passwords, no DaoAuthenticationProvider, and no database are involved.

In simple terms, spring-security-test does something like this: “before the test starts, I will prepare the SecurityContextHolder so that Spring Security thinks the user is already there.” And then your MockMvc.perform(...) follows the usual route through security filters and authorization — only now the request has “someone” attached, and you can check hasRole(...), authenticated(), and other rules.

An illustration could look like this:

flowchart TD
    A["JUnit Jupiter starts the test"] --> B["@WithMockUser prepares the SecurityContext"]
    B --> C["MockMvc.perform(request)"]
    C --> D[SecurityFilterChain]
    D --> E["Decision: 200 / 401 / 403"]
    E --> F["andExpect(status...)"]

Note that @WithMockUser is excellent when we check request-level access rules (public/personal/editor/admin zones) and want to quickly lock in the boundaries. But it will not help you verify that a “blocked user cannot log in,” because there is no login process here — you already have a “ready authenticated” user.

In practice, this means: if you want to test authorization quickly and at scale, @WithMockUser is your friend. If you want to test real authentication scenarios (login, account state, password, and so on), then you need a different approach, but we are not going there today.

3. @WithMockUser settings

@WithMockUser looks compact, but inside it has several important settings that you should know about, otherwise you will get the “wrong” 401/403 and suspect a framework conspiracy. It is important to understand three things: what the default values are, how roles are set, and how authorities behave.

By default, @WithMockUser creates a user with a name (usually "user") and the role USER. So this test:

@Test
@WithMockUser
void authenticatedUserCanOpenMe() throws Exception {
    // Check the access rule: there is authentication -> expect 200
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isOk());
}

reads like this: “an authenticated user (default) can open /api/me.” And that is a normal minimal smoke test for a personal area.

Roles: no ROLE_ prefix by hand

In @WithMockUser(roles = "..."), roles are specified without the ROLE_ prefix. That is, you write roles = "ADMIN", and Spring Security internally converts this to the authority ROLE_ADMIN. If you write roles = "ROLE_ADMIN", you will very likely get an exception or just strange behavior and an hour of philosophical questions about life.

Here is a good example of a role-based test for an admin zone:

@Test
@WithMockUser(roles = "ADMIN")
void adminCanReadUsers() throws Exception {
    // There is a user and the ADMIN role -> expect access to the admin zone
    mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
}

And here is the symmetric denial:

@Test
@WithMockUser(roles = "USER")
void ordinaryUserCannotReadUsers() throws Exception {
    // The user is authenticated, but the role is not enough -> expect 403
    mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden());
}

The difference is fundamental: the user exists, but there are not enough rights — so we expect 403.

Authorities: when they matter and how they affect roles

In a real project, we often use not only roles, but also more precise permissions (authorities), for example user:read, draft:publish, profile:write. @WithMockUser has an authorities field, and it is useful when your SecurityFilterChain or method security checks hasAuthority(...).

There is a subtlety here that beginners stumble over in a beautiful and loud way: if you set authorities, you are basically saying, “here are my permissions, do not try to infer them from roles.” That is why in most cases authorities become the main source of permissions.

Example (suppose some auxiliary endpoint is protected through hasAuthority("user:read"), rather than through a role-based rule):

@Test
@WithMockUser(authorities = "user:read")
void userReadAuthorityAllowsAccess() throws Exception {
    // Access is granted not by a role, but by a specific right (authority)
    mockMvc.perform(get("/api/support/users/search"))
            .andExpect(status().isOk());
}

If your endpoint is protected specifically through hasRole("ADMIN"), then the user:read authority may not be enough. So here it is important that the test reflects the real access rule of your application, not just “generally a good idea.”

Username: useful for readability, but it does not make you a “real user from the database”

You can set username so that the test is easier to read and so that /api/me returns the expected name (if your endpoint returns it). This is convenient when you are writing not one test, but a batch — the brain recognizes “anna / ed / admin” faster.

@Test
@WithMockUser(username = "anna", roles = "USER")
void namedUserIsAuthenticated() throws Exception {
    // The name affects the principal in SecurityContext, but does not load an entity from the database
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isOk());
}

But it is important to remember: the name "anna" here is not loading UserAccount from the database. It is just a name in mock authentication. That is, “inside the test” the user exists, but your database knows nothing about them unless you prepared it separately.

4. Three basic access scenarios

Security tests become very easy to understand if you keep in mind three basic states that we constantly check in a project: anonymous (nobody), authenticated (ordinary user), privileged (higher role/permission). But we do not need another full regression pack here. @WithMockUser is most useful where you need to show a short access rule without extra noise in the perform(...) itself.

Keep the access map the same: public stays public, /api/me requires authentication, editor/admin zones require elevated rights. That is enough to see when the annotation really makes the test cleaner.

Anonymous vs authenticated

An anonymous scenario in MockMvc is literally a request without @WithMockUser and without .with(user(...)). The most important point: a protected endpoint must return 401, not 403. That is, the problem is not with the rights, but with the fact that the user did not even identify themselves.

@Test
void anonymousCannotOpenMe() throws Exception {
    // No authentication -> expect 401 (must identify yourself)
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isUnauthorized());
}

Now the same endpoint, /api/me, but with a user:

@Test
@WithMockUser
void authenticatedUserCanOpenMe() throws Exception {
    // There is authentication (default user/USER) -> expect 200
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isOk());
}

This pair is good because the test reads almost like a declaration of the rule: without a user — 401, with a user — 200.

USER vs EDITOR

Next comes a normal role boundary: an ordinary user cannot, an editor can.

@Test
@WithMockUser(roles = "EDITOR")
void editorCanOpenReviewQueue() throws Exception {
    // The EDITOR role grants access to the editor zone
    mockMvc.perform(get("/api/editor/review-queue"))
            .andExpect(status().isOk());
}

And the matching denial:

@Test
@WithMockUser(roles = "USER")
void userCannotOpenReviewQueue() throws Exception {
    // The user exists, but the role does not fit -> expect 403
    mockMvc.perform(get("/api/editor/review-queue"))
            .andExpect(status().isForbidden());
}

Exactly the same approach is then used to fix stricter boundaries such as the admin zone. But the main point of @WithMockUser is already visible: the annotation removes noise around the user and leaves the access rule itself in the test.

5. @WithMockUser and user(): test style

When both user() and @WithMockUser appear in a project, beginners often get the feeling that “you have to choose one path forever.” In practice, this is not a religion, but a matter of readability and convenience. Both approaches solve the same task: they inject an authenticated user into the security context so that you can test authorization.

The difference is where you describe the user. user() lives directly inside the request and makes it very explicit. @WithMockUser lives at the test method level and makes the request “clean.” It is like choosing between “writing a variable name on every line” and “moving it into a constant” — both are fine as long as you do not turn the code into a mess.

Here is a short comparison that usually helps with the decision:

Question @WithMockUser user()
Where the user is set in the test annotation in a specific perform(...)
Request readability maximum, request is “clean” maximum clarity about “who came”
Convenient if one test = one user one test has many requests/users
Main risk forgetting which user is “hanging” on the test lots of repeated code

Sometimes it is useful to see the same test in both styles. First with the annotation:

@Test
@WithMockUser(roles = "EDITOR")
void editorCanOpenQueue_withAnnotation() throws Exception {
    // The user is set at the annotation level -> the request stays "clean"
    mockMvc.perform(get("/api/editor/review-queue"))
            .andExpect(status().isOk());
}

And then inside the request:

@Test
void editorCanOpenQueue_withUserPostProcessor() throws Exception {
    // The user is set inside perform(...) -> you can immediately see who is making the request
    mockMvc.perform(get("/api/editor/review-queue")
            .with(user("ed").roles("EDITOR")))
            .andExpect(status().isOk());
}

The meaning is the same. But the second option is a little louder, while making it immediately visible inside the request who is coming in. The first option is shorter and sometimes better suited for a short regression grid, where tests run one after another like access rules.

As soon as you build a regression pack from such tests, the question “which syntax is prettier” is over: you choose one style for the class and keep the whole set in it. Then the pack reads like a rule map, not like a showcase of different ways to inject a user.

6. Typical mistakes when working with @WithMockUser

Mistake #1: expecting @WithMockUser to verify real authentication.
This is one of the most common “mental bugs.” The annotation does not check the password, does not go to the database, and does not take account states into account. It simply creates the context “the user is already authenticated.” So if you are trying to test “locked user cannot log in,” you picked the wrong tool. For this lecture, we honestly test only authorization at the request level.

Mistake #2: writing roles with the ROLE_ prefix.
If you write @WithMockUser(roles = "ROLE_ADMIN"), you will almost certainly get odd behavior or a direct error. Roles are specified as "ADMIN", "EDITOR", "USER". The ROLE_ prefix is an internal convention that Spring Security adds itself, and it is better not to interfere with it.

Mistake #3: confusing 401 and 403 in expectations.
If an anonymous request reaches a personal or privileged zone, it is usually 401 (authentication required). If the user is authenticated but has the wrong role, it is 403. When a test expects 401 but gets 403, most often the reason is that you did inject a user after all (or @WithMockUser is applied at the class level), but forgot about it.

Mistake #4: making one “do everything” test.
Sometimes you want one method to check anonymous, user, editor, and admin all at once — especially if it seems “faster.” In practice, this makes tests fragile and hard to read. It is much more reliable when one test method = one scenario = one expected status. That makes it easier to understand exactly what broke.

Mistake #5: using @WithMockUser where the endpoint depends on your custom principal or database data.
If your controllers/services expect the principal to be your CustomUserDetails (with userId, roles from the database, and so on), then @WithMockUser may be insufficient: it creates an “ordinary” user. For request-level status checks this is often fine, but if you start testing more “domain-specific” things, you may suddenly find that the application expects a different principal type. Then @WithMockUser turns into a “too toy-like pass,” and the test either fails or starts checking the wrong thing.

1
Task
Spring Security, level 26, lesson 3
Locked
Anonymous and @WithMockUser for /api/me
Anonymous and @WithMockUser for /api/me
1
Task
Spring Security, level 26, lesson 3
Locked
Role-based access via @WithMockUser
Role-based access via @WithMockUser
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION