CodeGym /Cursos /Spring Security /@WithMockUser y roles en MockMvc

@WithMockUser y roles en MockMvc

Spring Security
Nivel 26, Lección 3
Disponible

1. @WithMockUser y user(): cuándo usar cada uno

Cuando ves por primera vez @WithMockUser, parece que es “otra anotación mágica” de la que se podría prescindir. En realidad, su aparición es muy práctica: a veces queremos que el test se lea como una regla de acceso pura, sin ruido. Es decir, que en el código solo quede el endpoint y el estado esperado, no las “decoraciones” alrededor del usuario.

Imagina que escribes una serie de tests cortos: «editor puede», «user no puede», «anonymous no puede». Si en cada test tienes que añadir constantemente .with(user("...").roles("...")), el código empieza a parecer un conjunto de conjuros similares. En cambio, @WithMockUser permite sacar «quién viene» arriba, en la anotación, y dejar en el test solo «qué comprobamos».

Hay otro punto importante: @WithMockUser no es un “inicio de sesión” ni una prueba de contraseña. Es más como un pase temporal para una conferencia. No pasas por un registro real en la recepción, simplemente te pones una etiqueta con EDITOR y el guardia te deja entrar a la sala adecuada. Para una gran cantidad de pruebas de autorización esto es exactamente lo que necesitas, porque en esos tests no nos interesa la contraseña, sino los límites del acceso.

Finalmente, en el equipo a menudo se percibe @WithMockUser como un “estilo corto estándar” que se lee más rápido. Si el objetivo es reunir un pack de regresión compacto, el estilo basado en anotaciones puede ser muy conveniente.

2. Modelo de funcionamiento de @WithMockUser

@WithMockUser tiene un superpoder y una super-salvedad. El superpoder es que, antes de ejecutar el método de test, coloca en el SecurityContext un objeto Authentication como si el usuario ya estuviera autenticado. La super-salvedad es que lo hace sin un login real: no intervienen contraseñas, DaoAuthenticationProvider ni base de datos.

En palabras sencillas, spring-security-test hace aproximadamente esto: «antes de ejecutar el test preparo el SecurityContextHolder para que Spring Security piense que el usuario ya existe». Luego tu MockMvc.perform(...) sigue la ruta habitual a través de los filtros de seguridad y la autorización, solo que ahora la solicitud tiene «alguien» y se pueden comprobar hasRole(...), authenticated() y demás reglas.

Esto se puede representar visualmente así:

flowchart TD
    A["JUnit Jupiter ejecuta test"] --> B["@WithMockUser prepara SecurityContext"]
    B --> C["MockMvc.perform(request)"]
    C --> D[SecurityFilterChain]
    D --> E["Decisión: 200 / 401 / 403"]
    E --> F["andExpect(status...)"]

Observa: @WithMockUser es ideal cuando comprobamos reglas de acceso a nivel de solicitud (zonas public/personal/editor/admin) y queremos fijar límites rápidamente. Pero no te ayudará a verificar que «un usuario bloqueado no inicia sesión», porque el proceso de login en sí no existe aquí — tienes directamente un “authenticated” ya listo.

En la práctica esto significa: si quieres probar la autorización de forma rápida y masiva, @WithMockUser es tu amigo. Si quieres probar escenarios reales de autenticación (login, estado de la cuenta, contraseña, etc.), entonces necesitas otro enfoque, pero hoy no entramos en eso.

3. Configuraciones de @WithMockUser

@WithMockUser se ve compacto, pero internamente tiene varios ajustes importantes que conviene conocer, de lo contrario recibirás «los equivocados» 401/403 y sospecharás de una conspiración del framework. Es importante entender tres cosas: los valores por defecto, cómo se definen los roles, y cómo se comportan las authorities.

Por defecto, @WithMockUser crea un usuario con nombre (normalmente "user") y rol USER. Por lo tanto, este test:

@Test
@WithMockUser
void authenticatedUserCanOpenMe() throws Exception {
    // Verificamos la regla de acceso: hay autenticación -> esperamos 200
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isOk());
}

se lee así: «un usuario autenticado (normal) puede abrir /api/me». Y es un smoke-test mínimo normal para una zona personal.

Roles: sin ROLE_ manualmente

En @WithMockUser(roles = "...") los roles se indican sin el prefijo ROLE_. Es decir, escribes roles = "ADMIN", y Spring Security internamente lo convierte en la authority ROLE_ADMIN. Si escribes roles = "ROLE_ADMIN", muy probablemente obtendrás una excepción o simplemente un comportamiento extraño, y una hora de preguntas filosóficas sobre la vida.

Aquí tienes un buen ejemplo de un test basado en roles para la zona admin:

@Test
@WithMockUser(roles = "ADMIN")
void adminCanReadUsers() throws Exception {
    // Hay usuario y rol ADMIN -> esperamos acceso a la zona de administración
    mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
}

Y junto a él, una prohibición simétrica:

@Test
@WithMockUser(roles = "USER")
void ordinaryUserCannotReadUsers() throws Exception {
    // El usuario está autenticado, pero no tiene el rol suficiente -> esperamos 403
    mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden());
}

La diferencia es fundamental: el usuario existe, pero no tiene permisos suficientes; por lo tanto, esperamos 403.

Authorities: cuándo son importantes y cómo afectan a los roles

En un proyecto real a menudo usamos no solo roles, sino también permisos más precisos (authorities), por ejemplo user:read, draft:publish, profile:write. @WithMockUser tiene el campo authorities, y es útil cuando tu SecurityFilterChain o la seguridad a nivel de método verifican precisamente hasAuthority(...).

Aquí hay un detalle con el que los novatos tropiezan de manera bonita y sonora: si defines authorities, estás diciendo «estos son mis permisos, no intentes deducirlos a partir de los roles». Por lo tanto, la mayoría de las veces authorities se convierte en la fuente principal de permisos.

Ejemplo (supongamos que algún endpoint auxiliar está protegido mediante hasAuthority("user:read"), no por una regla basada en roles):

@Test
@WithMockUser(authorities = "user:read")
void userReadAuthorityAllowsAccess() throws Exception {
    // El acceso se concede no por un rol, sino por un permiso específico (authority)
    mockMvc.perform(get("/api/support/users/search"))
            .andExpect(status().isOk());
}

Si tu endpoint está protegido precisamente mediante hasRole("ADMIN"), entonces la authority user:read puede ser insuficiente. Por eso es importante que el test refleje la regla de acceso real de tu aplicación, no «una buena idea en general».

Username: útil para legibilidad, pero no te convierte en un “usuario real de la BD”

Se puede definir username para que el test se lea mejor y para que /api/me devuelva el nombre esperado (si tu endpoint lo retorna). Es conveniente cuando no escribes un solo test sino un montón: el cerebro reconoce más rápido «anna / ed / admin».

@Test
@WithMockUser(username = "anna", roles = "USER")
void namedUserIsAuthenticated() throws Exception {
    // El nombre afecta al principal en SecurityContext, pero no carga la entidad de la BD
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isOk());
}

Pero es importante recordar: el nombre "anna" aquí no carga un UserAccount de la base de datos. Es solo un nombre en la autenticación simulada. Es decir, «dentro del test» el usuario existe, pero tu base de datos no sabe nada de él si no la has preparado aparte.

4. Tres escenarios básicos de acceso

Las pruebas de seguridad empiezan a ser muy claras si mantienes en mente tres estados básicos que comprobamos constantemente en un proyecto: anonymous (nadie), authenticated (usuario normal), privileged (rol/permiso superior). Pero aquí no necesitamos un segundo pack de regresión completo. @WithMockUser es más útil donde se necesita mostrar precisamente una regla de acceso corta sin ruido extra en el propio perform(...).

Mantenemos el mismo mapa de acceso: public sigue siendo public, /api/me requiere autenticación, las zonas editor/admin requieren permisos elevados. Esto es suficiente para ver cuándo la anotación realmente hace que el test sea más limpio.

Anonymous contra authenticated

El escenario anonymous en MockMvc es literalmente una solicitud sin @WithMockUser y sin .with(user(...)). El punto más importante: un endpoint protegido debe devolver 401, no 403. Es decir, el problema no son los permisos, sino que el usuario ni siquiera se ha identificado.

@Test
void anonymousCannotOpenMe() throws Exception {
    // Sin autenticación -> esperamos 401 (hay que identificarse)
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isUnauthorized());
}

Ahora el mismo endpoint /api/me, pero con usuario:

@Test
@WithMockUser
void authenticatedUserCanOpenMe() throws Exception {
    // Hay autenticación (por defecto user/USER) -> esperamos 200
    mockMvc.perform(get("/api/me"))
            .andExpect(status().isOk());
}

Este par es bueno porque el test se lee casi como una declaración de la regla: sin usuario — 401, con usuario — 200.

USER contra EDITOR

Luego aparece una frontera normal basada en roles: un usuario normal no puede, un editor sí puede.

@Test
@WithMockUser(roles = "EDITOR")
void editorCanOpenReviewQueue() throws Exception {
    // El rol EDITOR concede acceso a la zona editor
    mockMvc.perform(get("/api/editor/review-queue"))
            .andExpect(status().isOk());
}

Y su prohibición pareja:

@Test
@WithMockUser(roles = "USER")
void userCannotOpenReviewQueue() throws Exception {
    // El usuario existe, pero el rol no es adecuado -> esperamos 403
    mockMvc.perform(get("/api/editor/review-queue"))
            .andExpect(status().isForbidden());
}

De igual manera se fijan límites más estrictos como la zona admin. Pero el sentido principal de @WithMockUser ya se ve: la anotación elimina el ruido alrededor del usuario y deja en el test la regla de acceso misma.

5. @WithMockUser y user(): estilo del test

Cuando en el proyecto aparecen tanto user() como @WithMockUser, el novato suele sentir que «hay que elegir un camino para siempre». En la práctica no es una religión, sino una cuestión de legibilidad y comodidad. Ambos modos resuelven la misma tarea: colocar un usuario autenticado en el contexto de seguridad para que puedas probar la autorización.

La diferencia es dónde describes al usuario. user() vive dentro de la solicitud y la hace muy explícita. @WithMockUser vive a nivel del método de test y hace que la solicitud quede «limpia». Es como elegir entre «escribir el nombre de la variable en cada línea» y «ponerlo en una constante»; ambas opciones son válidas si no conviertes el código en un batiburrillo.

Aquí hay una comparación breve que suele ayudar a decidir:

Pregunta @WithMockUser user()
Dónde se define el usuario en la anotación del test en el perform(...) específico
Legibilidad de la solicitud máxima, solicitud «limpia» máxima claridad de «quién llegó»
Conveniente si un test = un usuario en un test muchas solicitudes/usuarios
Riesgo principal olvidar qué usuario «está asignado» al test mucho código repetitivo

A veces es útil ver el mismo test con los dos estilos. Primero con anotación:

@Test
@WithMockUser(roles = "EDITOR")
void editorCanOpenQueue_withAnnotation() throws Exception {
    // El usuario se define a nivel de anotación -> la solicitud queda «limpia»
    mockMvc.perform(get("/api/editor/review-queue"))
            .andExpect(status().isOk());
}

Y luego dentro de la solicitud:

@Test
void editorCanOpenQueue_withUserPostProcessor() throws Exception {
    // El usuario se define dentro de perform(...) -> se ve de inmediato quién hace la solicitud
    mockMvc.perform(get("/api/editor/review-queue")
            .with(user("ed").roles("EDITOR")))
            .andExpect(status().isOk());
}

El significado es el mismo. Pero la segunda opción es un poco más ruidosa, aunque dentro de la solicitud se ve de inmediato quién va. La primera opción es más corta y a veces encaja mejor en una malla de regresión corta donde los tests se suceden uno tras otro como reglas de acceso.

En cuanto reúnes estos tests en un pack de regresión, la cuestión de «qué sintaxis es más bonita» termina: eliges un estilo para la clase y mantienes todo el conjunto en él. Entonces el pack se lee como un mapa de reglas, no como un escaparate de distintas formas de poner un usuario.

6. Errores típicos al trabajar con @WithMockUser

Error #1: esperar que @WithMockUser verifique la autenticación real.
Es uno de los “bugs mentales” más frecuentes. La anotación no comprueba la contraseña, no accede a la BD ni tiene en cuenta el estado de la cuenta. Simplemente crea un contexto de «usuario ya autenticado». Por lo tanto, si intentas probar «usuario bloqueado no puede iniciar sesión», has elegido la herramienta equivocada. En esta lección probamos honestamente solo la autorización a nivel de solicitud.

Error #2: escribir roles con el prefijo ROLE_.
Si escribes @WithMockUser(roles = "ROLE_ADMIN"), casi con seguridad obtendrás rarezas o un error directo. Los roles se definen como "ADMIN", "EDITOR", "USER". El prefijo ROLE_ es una convención interna que Spring Security añade por sí mismo, y es mejor no interferir.

Error #3: confundir 401 y 403 en las expectativas.
Si una solicitud anónima llega a una zona personal o privilegiada, normalmente es 401 (hay que autenticarse). Si el usuario está autenticado pero no tiene el rol adecuado, es 403. Cuando un test espera 401 y recibe 403, la causa más frecuente es que igual has puesto un usuario (o te afecta un @WithMockUser a nivel de clase) y te has olvidado de ello.

Error #4: hacer un test “para todo a la vez”.
A veces se quiere probar en un solo método anonymous, user, editor y admin, sobre todo si parece que es «más rápido». En la práctica eso hace que los tests sean frágiles y difíciles de leer. Es mucho más fiable que un método de test = un escenario = un estado esperado. Así es más fácil entender qué se ha roto exactamente.

Error #5: usar @WithMockUser donde el endpoint depende de tu principal personalizado o de datos de la BD.
Si tus controladores/servicios esperan que el principal sea tu CustomUserDetails (con userId, roles de la BD, etc.), entonces @WithMockUser puede resultar insuficiente: crea un usuario «normal». Para comprobaciones de estado a nivel de solicitud a menudo es suficiente, pero si empiezas a probar cosas más «de dominio», de repente descubres que la aplicación espera otro tipo de principal. Entonces @WithMockUser se convierte en un «pase demasiado de juguete» y el test o falla o empieza a probar algo que no debe.

1
Tarea
Spring Security, nivel 26, lección 3
Bloqueada
Anonymous y @WithMockUser para /api/me
Anonymous y @WithMockUser para /api/me
1
Tarea
Spring Security, nivel 26, lección 3
Bloqueada
Acceso basado en roles mediante @WithMockUser
Acceso basado en roles mediante @WithMockUser
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION