Hands‑On Unit Tests for Full Spring Cache Features
This guide walks through setting up a Spring Boot 3.x project with JDK 17+, adding the necessary Maven dependencies, and implementing a comprehensive JUnit 5 test suite that demonstrates @Cacheable, @CachePut, @CacheEvict (single‑key and all‑entries), SpEL‑based keys, condition/unless caching, and the AOP proxy pitfall of same‑class calls, plus instructions for running the tests via Maven or IntelliJ IDEA.
Version and Environment
All test code targets Spring Boot 3.x + Spring Framework 6.x running on JDK 17+ . The test framework used is JUnit 5 with Spring Boot Test .
Environment Configuration
Maven Dependencies (pom.xml)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>Test Class
CacheTest – Full Cache Test Suite
The test class is ordered with @TestMethodOrder(MethodOrderer.OrderAnnotation.class) so that tests run sequentially.
package com.example.springcontainer.cache;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* Spring Cache annotation full test.
* Covers @Cacheable, @CachePut, @CacheEvict, SpEL keys, condition/unless, and AOP proxy issues.
*/
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@DisplayName("Spring Cache Full Test")
class CacheTest {
@Autowired
private CachedUserService cachedUserService;
// 1. @Cacheable – cache hit
@Test @Order(1) @DisplayName("Test 1: @Cacheable – first call stores, second call hits cache")
void cacheableCachesResult() {
Map<String, Object> user1 = cachedUserService.getUserById(1);
assertNotNull(user1);
assertEquals("Alice", user1.get("name"));
Map<String, Object> user2 = cachedUserService.getUserById(1);
assertSame(user1, user2, "Second call should hit cache and return same object");
System.out.println("Test 1 passed: @Cacheable works");
}
// 2. @CachePut – cache update
@Test @Order(2) @DisplayName("Test 2: @CachePut – method executes then updates cache")
void cachePutUpdatesCache() {
cachedUserService.getUserById(2); // populate null entry
Map<String, Object> updated = cachedUserService.updateUser(1, "Alice-Updated", "[email protected]");
assertNotNull(updated);
assertEquals("Alice-Updated", updated.get("name"));
Map<String, Object> user = cachedUserService.getUserById(1);
assertEquals("Alice-Updated", user.get("name"), "@CachePut should update cache");
System.out.println("Test 2 passed: @CachePut updates cache");
}
// 3. @CacheEvict – key delete
@Test @Order(3) @DisplayName("Test 3: @CacheEvict – delete by key")
void cacheEvictRemovesCache() {
Map<String, Object> user = cachedUserService.getUserById(3);
cachedUserService.deleteUser(3);
Map<String, Object> after = cachedUserService.getUserById(3);
assertNull(after);
System.out.println("Test 3 passed: @CacheEvict key delete works");
}
// 4. @CacheEvict allEntries – clear all
@Test @Order(4) @DisplayName("Test 4: @CacheEvict allEntries=true – clear all cache")
void cacheEvictAll() {
cachedUserService.getUserById(1);
cachedUserService.getUserById(2);
cachedUserService.clearAllCache();
Map<String, Object> user1 = cachedUserService.getUserById(1);
assertNotNull(user1);
System.out.println("Test 4 passed: allEntries clear works");
}
// 5. SpEL key expressions
@Test @Order(5) @DisplayName("Test 5: SpEL key expressions")
void spelExpression() {
Map<String, Object> u1 = cachedUserService.getUserById(1);
Map<String, Object> u1Again = cachedUserService.getUserById(1);
assertSame(u1, u1Again, "#id key should cache");
Map<String, Object> query = Map.of("id", 1, "name", "ignored");
Map<String, Object> r1 = cachedUserService.getUserByObject(query);
Map<String, Object> r2 = cachedUserService.getUserByObject(query);
assertSame(r1, r2, "#user['id'] key should cache");
MyUser myUser = new MyUser(1, null, null);
MyUser mu1 = cachedUserService.getUser(myUser);
MyUser mu2 = cachedUserService.getUser(myUser);
assertSame(mu1, mu2, "#myUser.id key should cache");
Map<String, Object> m1 = cachedUserService.getUserByIdWithMethod(1);
Map<String, Object> m2 = cachedUserService.getUserByIdWithMethod(1);
assertSame(m1, m2, "#root.methodName key should cache");
System.out.println("Test 5 passed: SpEL keys work");
}
// 6. condition – cache only when id > 0
@Test @Order(6) @DisplayName("Test 6: condition cache – id > 0 caches")
void conditionCache() {
Map<String, Object> p1 = cachedUserService.getUserWithCondition(1);
Map<String, Object> p1Again = cachedUserService.getUserWithCondition(1);
assertSame(p1, p1Again, "id > 0 should cache");
Map<String, Object> n1 = cachedUserService.getUserWithCondition(-1);
Map<String, Object> n2 = cachedUserService.getUserWithCondition(-1);
assertNull(n1);
assertNull(n2);
System.out.println("Test 6 passed: condition works");
}
// 7. unless – do not cache when result is null
@Test @Order(7) @DisplayName("Test 7: unless – null result not cached")
void unlessNotCache() {
Map<String, Object> u1 = cachedUserService.getUserWithUnless(1);
Map<String, Object> u1Again = cachedUserService.getUserWithUnless(1);
assertSame(u1, u1Again, "Non‑null result should cache");
Map<String, Object> miss1 = cachedUserService.getUserWithUnless(999);
Map<String, Object> miss2 = cachedUserService.getUserWithUnless(999);
assertNull(miss1);
assertNull(miss2);
System.out.println("Test 7 passed: unless works");
}
// 8. Same‑class call – AOP proxy issue
@Test @Order(8) @DisplayName("Test 8: Same‑class call – proxy bypass")
void sameClassCallCacheInvalid() {
Map<String, Object> d1 = cachedUserService.getUserById(1);
Map<String, Object> d2 = cachedUserService.getUserById(1);
assertSame(d1, d2, "Proxy call should hit cache");
Map<String, Object> i1 = cachedUserService.internalCall(1);
Map<String, Object> i2 = cachedUserService.internalCall(1);
assertSame(i1, i2, "Internal call bypasses proxy, cache not applied");
System.out.println("Test 8 passed: same‑class proxy issue demonstrated");
}
}Result Screenshot:
Auxiliary Classes
Cache Configuration
package com.example.springcontainer.cache;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
/** Enable Spring Cache support */
@Configuration
@EnableCaching
public class CacheConfig {
}Cache Service (business logic)
package com.example.springcontainer.cache;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/** Demonstrates all Spring Cache annotation scenarios */
@Service
public class CachedUserService {
private final Map<Integer, Map<String, Object>> users = new ConcurrentHashMap<>();
private final Map<Integer, MyUser> myUsers = new ConcurrentHashMap<>();
private final AtomicInteger idGenerator = new AtomicInteger(0);
{ // preset data
users.put(1, new ConcurrentHashMap<>(Map.of("id", 1, "name", "Alice", "email", "[email protected]")));
myUsers.put(1, new MyUser(1, "Alice", "[email protected]"));
myUsers.put(2, new MyUser(2, "Bob", "[email protected]"));
myUsers.put(3, new MyUser(3, "Charlie", "[email protected]"));
}
@Cacheable(value = "users", key = "#id")
public Map<String, Object> getUserById(int id) {
System.out.println("[CachedUserService] actual query, id=" + id);
return users.get(id);
}
@CachePut(value = "users", key = "#id")
public Map<String, Object> updateUser(int id, String name, String email) {
System.out.println("[CachedUserService] update user, id=" + id);
Map<String, Object> user = users.get(id);
if (user != null) {
user.put("name", name);
user.put("email", email);
}
return user;
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(int id) {
System.out.println("[CachedUserService] delete user, id=" + id);
users.remove(id);
}
@CacheEvict(value = "users", allEntries = true)
public void clearAllCache() {
System.out.println("[CachedUserService] clear all cache");
}
@Cacheable(value = "users", key = "#user['id']")
public Map<String, Object> getUserByObject(Map<String, Object> user) {
System.out.println("[CachedUserService] query with object, id=" + user.get("id"));
int id = (int) user.get("id");
return users.get(id);
}
@Cacheable(value = "myUsers", key = "#myUser.id")
public MyUser getUser(MyUser myUser) {
System.out.println("[CachedUserService] query MyUser, id=" + myUser.getId());
return myUsers.get(myUser.getId());
}
@Cacheable(value = "users", key = "#root.methodName + '_' + #id")
public Map<String, Object> getUserByIdWithMethod(int id) {
System.out.println("[CachedUserService] method‑name key, id=" + id);
return users.get(id);
}
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public Map<String, Object> getUserWithCondition(int id) {
System.out.println("[CachedUserService] condition query, id=" + id);
return users.get(id);
}
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public Map<String, Object> getUserWithUnless(int id) {
System.out.println("[CachedUserService] unless query, id=" + id);
return users.get(id);
}
public Map<String, Object> internalCall(int id) {
System.out.println("[CachedUserService] internal call, id=" + id);
return this.getUserById(id); // bypasses proxy
}
@Cacheable(value = "users", key = "#id")
private Map<String, Object> privateCacheMethod(int id) {
System.out.println("[CachedUserService] private method, id=" + id);
return users.get(id);
}
public Map<String, Object> callPrivateCacheMethod(int id) {
return this.privateCacheMethod(id);
}
}Entity Class
package com.example.springcontainer.cache;
public class MyUser {
private Integer id;
private String name;
private String email;
public MyUser(Integer id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}Running the Tests
Method 1: Maven Command
# Run a single test class
mvn test -Dtest=CacheTestMethod 2: IntelliJ IDEA
Open any test class.
Right‑click the class name or a test method.
Select “Run ‘XXX’” or “Debug ‘XXX’”.
Related Documentation
Spring Cache Full Analysis – detailed cache‑mechanism documentation.
Applicable versions: Spring Boot 3.x + Spring Framework 6.x + JDK 17+
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodeSmart Hoops
A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
