Java单元测试应隔离缓存副作用,专注验证业务逻辑:一、接口封装缓存时用Mockito模拟CacheService;二、注解缓存需抽离为可注入策略并Mock;三、静态缓存须重构为实例字段或Spring Bean管理。

Java单元测试对使用了缓存的业务逻辑进行测试,关键在于隔离缓存副作用、验证逻辑正确性、不依赖真实缓存行为。缓存本身不是被测逻辑的重点,而是外部依赖——它该被模拟(Mock)或替换为可控实现,而不是让测试跑在 Redis 或 Caffeine 上。
下面从实操角度分三类场景说明怎么做:
一、缓存调用被封装在独立接口中(推荐设计)
比如业务 Service 通过 CacheService 接口读写缓存,而具体实现是 RedisCacheServiceImpl 或 LocalCacheServiceImpl:
public class OrderService {
private final CacheService cacheService;
public OrderService(CacheService cacheService) {
this.cacheService = cacheService;
}
public Order getOrder(Long orderId) {
String key = "order:" + orderId;
Order cached = cacheService.get(key, Order.class);
if (cached != null) return cached;
Order dbOrder = orderDao.findById(orderId);
cacheService.set(key, dbOrder, 10, MINUTES);
return dbOrder;
}
}
✅ 测试做法:
- 用 Mockito Mock
CacheService,控制它的返回值和调用验证 - 专注验证:缓存命中时是否直接返回、未命中时是否查库并回填、是否设置正确过期时间
@Test
void shouldReturnFromCacheIfPresent() {
// 给 mock 的 cache 返回一个 Order
when(cacheService.get("order:123", Order.class)).thenReturn(new Order(123L, "PAID"));
Order result = service.getOrder(123L);
assertThat(result.getStatus()).isEqualTo("PAID");
verify(cacheService, never()).set(any(), any(), anyLong(), any());
}
@Test
void shouldLoadFromDbAndCacheIfAbsent() {
when(cacheService.get("order:456", Order.class)).thenReturn(null);
when(orderDao.findById(456L)).thenReturn(new Order(456L, "SHIPPED"));
Order result = service.getOrder(456L);
assertThat(result.getStatus()).isEqualTo("SHIPPED");
verify(cacheService).set(eq("order:456"), any(Order.class), eq(10L), eq(MINUTES));
}
二、缓存注解直接写在方法上(如 @CreateCache / @Cached)
常见于 JetCache、Caffeine、Spring Cache 场景。例如:
@CreateCache(name = "userCache", expire = 60)
private Cache<long user> userCache;
public User getUser(Long id) {
return userCache.get(id, () -> userDao.findById(id));
}</long>
⚠️ 单元测试不能直接测这个 userCache 字段(它是运行时注入的代理对象),否则会触发真实缓存行为。
✅ 正确做法:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
-
不要测试缓存注解本身(那是框架的事),只测
userDao.findById(id)是否被正确调用 - 把
userCache声明为protected或包级访问,测试中用ReflectionTestUtils.setField()注入一个MockCache实例 - 或更干净的方式:将缓存逻辑抽成可注入策略,如
CacheLoader,测试时传入id -> mockUser
示例(JetCache 风格):
// 改造成可注入
public class UserService {
private final Cache<long user> userCache;
private final Function<long user> loader;
public UserService(Cache<long user> cache, Function<long user> loader) {
this.userCache = cache;
this.loader = loader;
}
public User getUser(Long id) {
return userCache.get(id, () -> loader.apply(id));
}
}</long></long></long></long>
测试时传入 mock(Cache.class) 和 id -> new User(id, "test") 即可完全控制。
三、静态缓存或单例缓存(高风险,需重构)
比如:
public class ConfigManager {
private static final Map<string string> cache = new ConcurrentHashMap();
public static String get(String key) { ... }
}</string>
❌ 单元测试中直接调用 ConfigManager.get() 会导致状态污染(多个测试共享同一 map)
✅ 解法不是清空 map,而是:
- 将
cache变为实例字段,通过构造器注入(如new ConfigManager(new ConcurrentHashMap())) - 或改用
ThreadLocal,每个测试线程独享副本 - 或彻底移除静态缓存,改用 Spring Bean 管理生命周期,测试时用
@MockBean替换
@SpringBootTest
class ConfigManagerTest {
@MockBean private ConfigRepository configRepo;
@Test
void shouldLoadFromRepoWhenNotInCache() {
when(configRepo.findByKey("timeout")).thenReturn("3000");
assertThat(configManager.get("timeout")).isEqualTo("3000");
// 第二次调用应走缓存(若已实现)
assertThat(configManager.get("timeout")).isEqualTo("3000");
verify(configRepo, times(1)).findByKey("timeout");
}
}
注意:这种测试已属于集成/组件测试范畴,需启动 Spring 上下文;纯单元测试仍建议走构造注入 + Mock 方式。
不复杂但容易忽略:缓存只是加速手段,业务逻辑的正确性不依赖它是否存在;测试要守住这个边界,才能又快又稳。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










