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);
}

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章