通常編寫單元測試主要是針對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); }