You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 13:06:05