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

如何获取Caffeine缓存中键的不可变快照以完成测试断言

Caffeine缓存键快照获取与过期测试失败解决

问题场景

我用Caffeine缓存实现了一个简单的注册表:

class LocalFactorsRegistry(
    duration: Duration,
) : FactorsRegistry {
    private val factors =
        Caffeine
            .newBuilder()
            .expireAfterWrite(duration)
            .build<String, Boolean>()

    override fun getUsedFactors(): Set<String> = factors.asMap().keys

    override fun markFactorAsUsed(factorId: String) {
        factors.put(factorId, true)
    }
}

为测试缓存项是否会被驱逐,我编写了如下测试代码:

@Test
fun `should mark factor as used and wait for expiration`() {
    val factory = LocalFactorsRegistry(Duration.ofMillis(100))

    factory.getUsedFactors() shouldBe emptySet()

    factory.markFactorAsUsed("factor2")

    Thread.sleep(100)

    factory.getUsedFactors() shouldBe emptySet()
}

但测试在最后一个断言环节失败,抛出异常:

java.util.NoSuchElementException
    at com.github.benmanes.caffeine.cache.BoundedLocalCache$EntryIterator.nextKey(BoundedLocalCache.java:3754)
    at com.github.benmanes.caffeine.cache.BoundedLocalCache$KeyIterator.next(BoundedLocalCache.java:3350)
    at kotlin.collections.CollectionsKt___CollectionsKt.toList(_Collections.kt:1323)
    at io.kotest.assertions.print.IterablePrint.print(IterablePrint.kt:6)
    at io.kotest.assertions.print.IterablePrint.print(IterablePrint.kt:3)
    at io.kotest.assertions.print.PrintKt.print(Print.kt:48)
    at io.kotest.assertions.eq.IterableEq.generateError(IterableEq.kt:221)
    at io.kotest.assertions.eq.IterableEq.checkSetEquality(IterableEq.kt:77)
    at io.kotest.assertions.eq.IterableEq.equals(IterableEq.kt:65)
    at io.kotest.assertions.eq.EqKt.eq(Eq.kt:51)
    at io.kotest.assertions.eq.EqKt.eq(Eq.kt:66)
    at io.kotest.matchers.ShouldKt$equalityMatcher$1.test(should.kt:73)
    at io.kotest.matchers.ShouldKt.invokeMatcher(should.kt:38)
    at io.kotest.matchers.ShouldKt.should(should.kt:33)
    at io.kotest.matchers.ShouldKt.shouldBe(should.kt:17)
    at com.example.LocalFactorsRegistryTest.should mark factor as used and wait for expiration(AbstractFactorsRegistryTest.kt:35)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

我需要知道如何获取Caffeine缓存中键的不可变快照,以正确完成断言。

解决方案

1. 生成不可变快照的正确方式

问题根源在于factors.asMap().keys返回的是缓存的实时视图,而非独立快照。当Kotest遍历这个视图进行断言时,缓存的过期清理操作可能刚好在遍历中途执行,导致迭代器抛出NoSuchElementException。

要获取不可变快照,只需将实时视图的键集合复制到新的不可变集合中,修改getUsedFactors方法:

// 方式1:用Java HashSet复制生成快照
override fun getUsedFactors(): Set<String> = HashSet(factors.asMap().keys)

// 方式2:用Kotlin扩展函数生成不可变Set
override fun getUsedFactors(): Set<String> = factors.asMap().keys.toSet()

两种方式都会生成当前缓存键的独立副本,后续缓存的过期操作不会影响这个快照,断言时就不会出现迭代异常。

2. 优化测试稳定性

Caffeine的过期清理是惰性触发的(通常在缓存被访问时执行),仅靠Thread.sleep(100)无法保证过期项已经被清理。可以在断言前主动触发一次缓存访问,确保过期项被清理:

@Test
fun `should mark factor as used and wait for expiration`() {
    val factory = LocalFactorsRegistry(Duration.ofMillis(100))

    factory.getUsedFactors() shouldBe emptySet()

    factory.markFactorAsUsed("factor2")

    Thread.sleep(150) // 适当延长等待时间,确保过期时间窗口已过
    factory.getUsedFactors() // 触发缓存清理
    factory.getUsedFactors() shouldBe emptySet()
}

也可以直接调用Caffeine的cleanUp()方法(需要将factors的访问权限调整为internal,或者在测试中通过反射调用),不过上述方式更简洁且无需修改类的封装性。


内容的提问来源于stack exchange,提问作者pixel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 18:32:01