通常编写单元测试主要是针对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); }