guava缓存编写单元测试遇到的问题

      通常编写单元测试主要是针对service类,因为主要的业务逻辑都在service层;单元测试往往要求达到一定的覆盖率,主要包括方法覆盖率和分支覆盖率。分支覆盖率只要是指业务逻辑中的各种情况(例如if...else...等等),各种条件下如果都能执行到,那么你的测试覆盖率一定会非常高。

      现在来看一个例子,我的业务逻辑中使用了多级缓存,首先从guava中读取,如果没有再从redis中获取,再没有则从数据库获取;然后依次放入缓存中,下次来直接去guava中获取。我的项目没有将guava封装成一个全局的工具类,而是选择在每一个service中单独创建一个guava实例。

       注意在存放的value对象外面我包裹了一个Optional<>,这是因为guava不能返回null,否则会报错。guava缓存实例如下代码所示:

private static Cache<String, Optional<User>> localCache = CacheBuilder.newBuilder()
        // 并发级别为8
        .concurrencyLevel(8)
        // 写缓存1分钟后过期
        .expireAfterWrite(1, TimeUnit.MINUTES)
        // 缓存初始容量为10
        .initialCapacity(10)
        // 缓存最大容量为100,超过以后按照lru算法移除缓存
        .maximumSize(100)
        // 统计缓存命中率
        .recordStats()
        // 缓存移除时打印日志
        .removalListener(removalNotification -> log.info("guava---key="
                + removalNotification.getKey() + " was removed, cause is "
                + removalNotification.getCause())).build();

      主要的业务逻辑代码如下:

Optional<User> user = Optional.empty();
try {
    // 首先从guava中获取
    user = localCache.get(key, () -> {
        // guava中不存在,则从redis中获取
        if (redisService.exist(key)) {
            User user1 = redisService.get(key);
            localCache.put(key, Optional.of(user1));
            return Optional.of(user1);
        }
        // redis中不存在,则从数据库中获取
        User user2 = userMapper.findUserByName(name);
        if (null != user2) {
            redisService.set(key, user2, 300);
            localCache.put(key, Optional.of(user2));
        }
        return Optional.ofNullable(user2);
    });
} catch (Exception e) {
    log.error("UserServiceImpl: findUserByName occur error:{}", e.getMessage());
}
// 如果user存在就返回(而不管是从哪个缓存或数据库中获取的),不存在就返回null(optional的好处)
return user.orElse(null);

      现在来看看,我当时单元测试想覆盖所有的分支,那么我就必须模拟一下三种情况:

1.guava不存在,redis 不存在,数据库存在

2.由于第一种情况数据库已经存在,查出来后肯定放到redis和guava中了,那么接下来模拟guava不存在,redis存在

3.由于第二种情况redis已存在,那么肯定查出来会放到guava中,接下来模拟guava存在

    当然还有一种情况是上面三个都不存在,也可以模拟。

    在模拟的时候我遇到一个问题就是,我service类中guava的缓存实例设置的是1分钟过期,但是我单测中无法拿到service类中的guava实例,要不然我可以编写一个类继承Ticker(这个ticker是guava缓存的时钟计时类)、重写它的read方法,在当前时刻加上1分钟在给他赋值,让它立即流逝1分钟,达到缓存过期的效果,如下所示:

public class TestTicker extends Ticker {
    private long start = Ticker.systemTicker().read();
    private long elapsedNano = 0;

    @Override
    public long read() {
        return start + elapsedNano;
    }

    public void addElapsedTime(long elapsedNano) {
        this.elapsedNano = elapsedNano;
    }
}

     随后这样使用: 

TestTicker testTicker = new TestTicker();

Cache<String, String> cache = CacheBuilder.newBuilder()
        // 将你自定义的ticker设置给实例
        .ticker(testTicker)
        .expireAfterAccess(1, TimeUnit.MINUTES)
        .build();
// 模拟流逝1分钟
testTicker.addElapsedTime(TimeUnit.NANOSECONDS.convert(1,TimeUnit.MINUTES));

     但是因为我的guava并不是全局的,而是各个业务的单独实例。所以没办法采用上面的方式,经过别人的点拨,我恍然大悟,我可以用随机数的key来获取缓存,为什么非要用相同的key呢,这就好办了,由于单元测试用的是mock模拟数据,那么key不同又有什么关系呢,只要逻辑正确就ok,最终单元测试代码如下:

private UserServiceImpl userService = new UserServiceImpl();

private UserMapper userMapper = mock(UserMapper.class);

private RedisService redisService = mock(RedisService.class);

@Before
public void setUp() {
    ReflectionTestUtils.setField(userService, "userMapper",userMapper);
    ReflectionTestUtils.setField(userService, "redisService", redisService);
}
@Test
public void testGetUserByUserId() {
    long userId = 13L;
    String key = "p_user_" + String.valueOf(userId);
    User user = new User();
    user.setName("zhangsan");
    user.setUserPortrait("/sdf/sdf");

    // 数据库有
    when(userMapper.findUserByUserId(userId)).thenReturn(user);
    User user2 = userService.findUserPortraitByUserId(userId);
    Assert.assertNotNull("result", user2);

    // redis有
    long userId2 = 14L;
    String key2 = "p_user_" + String.valueOf(userId2);
    when(redisService.exist(key2)).thenReturn(true);
    when(redisService.get(key2)).thenReturn(user);
    User user3 = userService.findUserPortraitByUserId(userId2);
    Assert.assertNotNull("result", user3);

    // guava有
    User user4 = userService.findUserPortraitByUserId(userId);
    Assert.assertNotNull("result", user4);
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值