如何测试Guava LoadingCache仅调用指定加载方法一次
问题根因
你初始化LoadingCache时传入的this::doTask绑定的是原始未被Spy包装的TaskService实例的方法,缓存触发回调时走的是原始对象的方法,完全绕开了Spy的代理逻辑,所以你对Spy对象的校验、Stub规则都不会生效。
注意:所有缓存校验方案都要求
TaskDetails类正确实现了equals()和hashCode()方法,否则Guava Cache会认为每次的Key不同,自然会多次触发加载逻辑,符合你提到的哈希计算不符合预期的场景。
解决方案
方案1:利用现有内部依赖验证(无需修改业务代码,最省事)
doTask执行必然会触发内部依赖something的方法调用,直接Mock这个依赖注入到TaskService,校验该依赖的调用次数即可:
@Mock private Something something; @Test public void testCacheEffect() { // given TaskService taskService = new TaskService(something, ... /* 其他构造参数 */); TaskDetails taskDetails = new TaskDetails(...); // 构造相同的入参 // when for (int i = 0; i < 3; i++) { taskService.taskCaller(taskDetails); } // then // 缓存生效的话,something的方法只会被调用1次 verify(something, times(1)).doSomething(); }
方案2:重构业务代码提升可测性(推荐,长期可维护性更好)
将CacheLoader作为依赖注入到TaskService中,测试时可以自定义带计数的CacheLoader验证逻辑:
重构后业务代码:
public class TaskService { private final Something something; private final LoadingCache<TaskDetails, byte[]> cache; // 生产环境用的公开构造函数 public TaskService(int cacheSize, int cacheDurationInMinutes, Something something) { this(cacheSize, cacheDurationInMinutes, something, CacheLoader.from(taskDetails -> doTask(taskDetails))); } // 测试专用的包级私有构造函数 TaskService(int cacheSize, int cacheDurationInMinutes, Something something, CacheLoader<TaskDetails, byte[]> cacheLoader) { this.something = something; this.cache = CacheBuilder.newBuilder() .maximumSize(cacheSize) .expireAfterAccess(Duration.ofMinutes(cacheDurationInMinutes)) .build(cacheLoader); } public byte[] taskCaller(TaskDetails taskDetails) { return cache.getUnchecked(taskDetails); } public byte[] doTask(TaskDetails taskDetails) { if(something.doSomething()) { // 业务逻辑 } // 返回结果 } }
对应的测试代码:
@Test public void testCacheEffect() { // given final int[] invokeCounter = {0}; CacheLoader<TaskDetails, byte[]> testLoader = new CacheLoader<>() { @Override public byte[] load(TaskDetails key) { invokeCounter[0]++; return new byte[]{0,0,0,0}; } }; TaskService taskService = new TaskService(10, 5, mock(Something.class), testLoader); TaskDetails sameTask = new TaskDetails(...); // when for (int i = 0; i < 3; i++) { taskService.taskCaller(sameTask); } // then assertThat(invokeCounter[0]).isEqualTo(1); }
方案3:反射替换CacheLoader(不推荐,仅适合不想改业务代码的临时场景)
如果因为历史原因不能修改业务代码,可以用反射修改TaskService内部的cache字段,替换为你自己构造的带计数逻辑的LoadingCache,逻辑和方案2一致,只是用反射注入而不是构造函数传入。
内容的提问来源于stack exchange,提问作者jeffrey.d.m
相关产品推荐
相关产品推荐

