Saltar al contenido principal

Probar con caché real

Sin Docker: spring.cache.type: simple​

spring:
cache:
type: simple

Fuerza ConcurrentMapCacheManager, el CacheManager que Spring Boot autoconfigura cuando no hay (o no se quiere usar) un backend externo: un Map en memoria por caché, sin serialización de por medio. Es lo que usan tanto el perfil de test (src/test/resources/application.yml) como el perfil h2 (application-h2.yml) — mismas anotaciones @Cacheable/@CacheEvict de producción, funcionando contra una caché real, no un mock, sin necesitar Redis.

Confirmar la caché contra el CacheManager real, no contra un mock​

@Autowired
private CacheManager cacheManager;

private Cache tasksCache() {
Cache cache = cacheManager.getCache("tasks");
assertThat(cache).isNotNull();
return cache;
}

@Test
void getTaskById_populatesCache() throws Exception {
String token = registerAndLogin("cara");
Long id = createTask(token, "Tarea de cara");
assertThat(tasksCache().get(id)).isNull();

mockMvc.perform(get("/tasks/{id}", id).header("Authorization", "Bearer " + token))
.andExpect(status().isOk());

assertThat(tasksCache().get(id)).isNotNull();
}

Inyectar el CacheManager y preguntarle directamente por la entrada (cacheManager.getCache("tasks").get(id)) prueba el comportamiento real de la abstracción de caché — nada de esto pasaría si @Cacheable estuviera silenciosamente desactivado por la trampa de auto-invocación descrita en Spring Cache básico; el test fallaría de inmediato porque la entrada nunca aparecería.

El mismo enfoque confirma la invalidación:

@Test
void updateTask_evictsCacheEntry() throws Exception {
// ... crear la tarea, leerla (puebla la caché) ...
assertThat(tasksCache().get(id)).isNotNull();

// ... PUT /tasks/{id} ...

assertThat(tasksCache().get(id)).isNull();
}

Y, el test más importante de esta fase, que un acierto de caché nunca se sirve a quien no tiene permiso (ver Invalidación de caché):

@Test
void cacheHit_stillEnforcesOwnership_forDifferentUser() throws Exception {
// primera lectura del dueño: cache miss
mockMvc.perform(get("/tasks/{id}", id).header("Authorization", "Bearer " + ownerToken))
.andExpect(status().isOk());

// segunda lectura del MISMO id: acierto de caché, pero de alguien sin permiso
mockMvc.perform(get("/tasks/{id}", id).header("Authorization", "Bearer " + otherToken))
.andExpect(status().isForbidden());
}

Verificación manual contra Redis real​

Con Docker disponible y docker compose up -d corriendo, desde la carpeta del ejemplo (redis-cli se ejecuta dentro del contenedor, no hace falta instalarlo):

docker compose exec redis redis-cli keys "tasks::*"
docker compose exec redis redis-cli get "tasks::1"

Tras un GET /tasks/1, debería aparecer una clave tasks::1 con el JSON de la tarea (gracias a GenericJacksonJsonRedisSerializer, ver Redis como backend); tras un PUT/DELETE sobre esa misma tarea, la clave desaparece. El JSON que aparece ahí incluye el user de la tarea, pero no su contraseña: el getter lleva @JsonIgnore precisamente porque este Redis de ejemplo corre sin autenticación y nada aguas abajo necesita el hash. Que ese user sea un User real y no un proxy perezoso es lo que garantiza el join fetch de findByIdWithUser (ver Spring Cache básico); ambas cosas están cubiertas por un test unitario de serialización (CacheValueSerializationTest) que ejercita el mismo serializador sin necesitar Redis.

Esta misma comprobación —que la clave aparece tras un GET y desaparece tras un PUT— también existe como test automático contra un Redis real: TaskApiIT, en la Fase 6, levanta Redis con Testcontainers y ejecuta redis-cli dentro del contenedor (ver Testcontainers).