安装
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill unit-test-caching
复制
Unit Testing Spring Caching
Overview
This skill provides patterns for unit testing Spring caching annotations (@Cacheable, @CacheEvict, @CachePut) without full Spring context. It covers testing cache behavior, hits/misses, invalidation strategies, cache key generation, and conditional caching using in-memory cache managers.
When to Use
Use this skill when:
Testing @Cacheable method caching
Testing @CacheEvict cache invalidation
Testing @CachePut cache updates
Verifying cache key generation
Testing conditional caching
Want fast caching tests without Redis or cache infrastructure
Instructions
Use in-memory CacheManager
Use ConcurrentMapCacheManager for tests instead of Redis or other external caches
Verify repository call counts
Use
times(n)
to verify cache hits/misses by counting repository invocations
Test both cache and eviction scenarios
Verify data is cached on first call and evicted when appropriate
Test cache key generation
Ensure SpEL expressions in
@Cacheable(key = "...")
produce correct keys
Test conditional caching
Verify
unless
and
condition
parameters work correctly
Clear cache between tests
Reset cache state in @BeforeEach or use @DirtiesContext
Mock service dependencies
Use mocks for repositories and other services the caching layer uses
Verify cache behavior explicitly
Don't rely on timing; verify actual cache hit/miss behavior
Examples
Setup: Caching Testing
Maven
<
dependency
>
<
groupId
>
org.springframework.boot
</
groupId
>
<
artifactId
>
spring-boot-starter-cache
</
artifactId
>
</
dependency
>
<
dependency
>
<
groupId
>
org.springframework.boot
</
groupId
>
<
artifactId
>
spring-boot-starter-test
</
artifactId
>
<
scope
>
test
</
scope
>
</
dependency
>
<
dependency
>
<
groupId
>
org.mockito
</
groupId
>
<
artifactId
>
mockito-core
</
artifactId
>
<
scope
>
test
</
scope
>
</
dependency
>
<
dependency
>
<
groupId
>
org.assertj
</
groupId
>
<
artifactId
>
assertj-core
</
artifactId
>
<
scope
>
test
</
scope
>
</
dependency
>
Gradle
dependencies
{
implementation
(
"org.springframework.boot:spring-boot-starter-cache"
)
testImplementation
(
"org.springframework.boot:spring-boot-starter-test"
)
testImplementation
(
"org.mockito:mockito-core"
)
testImplementation
(
"org.assertj:assertj-core"
)
}
Basic Pattern: Testing @Cacheable
Cache Hit and Miss Behavior
// Service with caching
@Service
public
class
UserService
{
private
final
UserRepository
userRepository
;
public
UserService
(
UserRepository
userRepository
)
{
this
.
userRepository
=
userRepository
;
}
@Cacheable
(
"users"
)
public
User
getUserById
(
Long
id
)
{
return
userRepository
.
findById
(
id
)
.
orElse
(
null
)
;
}
}
// Test caching behavior
import
org
.
junit
.
jupiter
.
api
.
Test
;
import
org
.
junit
.
jupiter
.
api
.
BeforeEach
;
import
org
.
springframework
.
cache
.
CacheManager
;
import
org
.
springframework
.
cache
.
annotation
.
EnableCaching
;
import
org
.
springframework
.
cache
.
concurrent
.
ConcurrentMapCacheManager
;
import
org
.
springframework
.
context
.
annotation
.
Bean
;
import
org
.
springframework
.
context
.
annotation
.
Configuration
;
import
static
org
.
mockito
.
Mockito
.
*
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
*
;
@Configuration
@EnableCaching
class
CacheTestConfig
{
@Bean
public
CacheManager
cacheManager
(
)
{
return
new
ConcurrentMapCacheManager
(
"users"
)
;
}
}
class
UserServiceCachingTest
{
private
UserRepository
userRepository
;
private
UserService
userService
;
private
CacheManager
cacheManager
;
@BeforeEach
void
setUp
(
)
{
userRepository
=
mock
(
UserRepository
.
class
)
;
cacheManager
=
new
ConcurrentMapCacheManager
(
"users"
)
;
userService
=
new
UserService
(
userRepository
)
;
}
@Test
void
shouldCacheUserAfterFirstCall
(
)
{
User
user
=
new
User
(
1L
,
"Alice"
)
;
when
(
userRepository
.
findById
(
1L
)
)
.
thenReturn
(
Optional
.
of
(
user
)
)
;
User
firstCall
=
userService
.
getUserById
(
1L
)
;
User
secondCall
=
userService
.
getUserById
(
1L
)
;
assertThat
(
firstCall
)
.
isEqualTo
(
secondCall
)
;
verify
(
userRepository
,
times
(
1
)
)
.
findById
(
1L
)
;
// Called only once due to cache
}
@Test
void
shouldReturnCachedValueOnSecondCall
(
)
{
User
user
=
new
User
(
1L
,
"Alice"
)
;
when
(
userRepository
.
findById
(
1L
)
)
.
thenReturn
(
Optional
.
of
(
user
)
)
;
userService
.
getUserById
(
1L
)
;
// First call - hits database
User
cachedResult
=
userService
.
getUserById
(
1L
)
;
// Second call - hits cache
assertThat
(
cachedResult
)
.
isEqualTo
(
user
)
;
verify
(
userRepository
,
times
(
1
)
)
.
findById
(
1L
)
;
}
}
Testing @CacheEvict
Cache Invalidation
@Service
public
class
ProductService
{
private
final
ProductRepository
productRepository
;
public
ProductService
(
ProductRepository
productRepository
)
{
this
.
productRepository
=
productRepository
;
}
@Cacheable
(
"products"
)
public
Product
getProductById
(
Long
id
)
{
return
productRepository
.
findById
(
id
)
.
orElse
(
null
)
;
}
@CacheEvict
(
"products"
)
public
void
deleteProduct
(
Long
id
)
{
productRepository
.
deleteById
(
id
)
;
}
@CacheEvict
(
value
=
"products"
,
allEntries
=
true
)
public
void
clearAllProducts
(
)
{
// Clear entire cache
}
}
class
ProductCacheEvictTest
{
private
ProductRepository
productRepository
;
private
ProductService
productService
;
private
CacheManager
cacheManager
;
@BeforeEach
void
setUp
(
)
{
productRepository
=
mock
(
ProductRepository
.
class
)
;
cacheManager
=
new
ConcurrentMapCacheManager
(
"products"
)
;
productService
=
new
ProductService
(
productRepository
)
;
}
@Test
void
shouldEvictProductFromCacheWhenDeleted
(
)
{
Product
product
=
new
Product
(
1L
,
"Laptop"
,
999.99
)
;
when
(
productRepository
.
findById
(
1L
)
)
.
thenReturn
(
Optional
.
of
(
product
)
)
;
productService
.
getProductById
(
1L
)
;
// Cache the product
productService
.
deleteProduct
(
1L
)
;
// Evict from cache
User
cachedAfterEvict
=
userService
.
getUserById
(
1L
)
;
// After eviction, repository should be called again
verify
(
productRepository
,
times
(
2
)
)
.
findById
(
1L
)
;
}
@Test
void
shouldClearAllEntriesFromCache
(
)
{
Product
product1
=
new
Product
(
1L
,
"Laptop"
,
999.99
)
;
Product
product2
=
new
Product
(
2L
,
"Mouse"
,
29.99
)
;
when
(
productRepository
.
findById
(
1L
)
)
.
thenReturn
(
Optional
.
of
(
product1
)
)
;
when
(
productRepository
.
findById
(
2L
)
)
.
thenReturn
(
Optional
.
of
(
product2
)
)
;
productService
.
getProductById
(
1L
)
;
productService
.
getProductById
(
2L
)
;
productService
.
clearAllProducts
(
)
;
// Clear all cache entries
productService
.
getProductById
(
1L
)
;
productService
.
getProductById
(
2L
)
;
// Repository called twice for each product
verify
(
productRepository
,
times
(
2
)
)
.
findById
(
1L
)
;
verify
(
productRepository
,
times
(
2
)
)
.
findById
(
2L
)
;
}
}
Testing @CachePut
Cache Update
@Service
public
class
OrderService
{
private
final
OrderRepository
orderRepository
;
public
OrderService
(
OrderRepository
orderRepository
)
{
this
.
orderRepository
=
orderRepository
;
}
@Cacheable
(
"orders"
)
public
Order
getOrder
(
Long
id
)
{
return
orderRepository
.
findById
(
id
)
.
orElse
(
null
)
;
}
@CachePut
(
value
=
"orders"
,
key
=
"#order.id"
)
public
Order
updateOrder
(
Order
order
)
{
return
orderRepository
.
save
(
order
)
;
}
}
class
OrderCachePutTest
{
private
OrderRepository
orderRepository
;
private
OrderService
orderService
;
@BeforeEach
void
setUp
(
)
{
orderRepository
=
mock
(
OrderRepository
.
class
)
;
orderService
=
new
OrderService
(
orderRepository
)
;
}
@Test
void
shouldUpdateCacheWhenOrderIsUpdated
(
)
{
Order
originalOrder
=
new
Order
(
1L
,
"Pending"
,
100.0
)
;
Order
updatedOrder
=
new
Order
(
1L
,
"Shipped"
,
100.0
)
;
when
(
orderRepository
.
findById
(
1L
)
)
.
thenReturn
(
Optional
.
of
(
originalOrder
)
)
;
when
(
orderRepository
.
save
(
updatedOrder
)
)
.
thenReturn
(
updatedOrder
)
;
orderService
.
getOrder
(
1L
)
;
Order
result
=
orderService
.
updateOrder
(
updatedOrder
)
;
assertThat
(
result
.
getStatus
(
)
)
.
isEqualTo
(
"Shipped"
)
;
// Next call should return updated version from cache
Order
cachedOrder
=
orderService
.
getOrder
(
1L
)
;
assertThat
(
cachedOrder
.
getStatus
(
)
)
.
isEqualTo
(
"Shipped"
)
;
}
}
Testing Conditional Caching
Cache with Conditions
@Service
public
class
DataService
{
private
final
DataRepository
dataRepository
;
public
DataService
(
DataRepository
dataRepository
)
{
this
.
dataRepository
=
dataRepository
;
}
@Cacheable
(
value
=
"data"
,
unless
=
"#result == null"
)
public
Data
getData
(
Long
id
)
{
return
dataRepository
.
findById
(
id
)
.
orElse
(
null
)
;
}
@Cacheable
(
value
=
"users"
,
condition
=
"#id > 0"
)
public
User
getUser
(
Long
id
)
{
return
userRepository
.
findById
(
id
)
.
orElse
(
null
)
;
}
}
class
ConditionalCachingTest
{
@Test
void
shouldNotCacheNullResults
(
)
{
DataRepository
dataRepository
=
mock
(
DataRepository
.
class
)
;
when
(
dataRepository
.
findById
(
999L
)
)
.
thenReturn
(
Optional
.
empty
(
)
)
;
DataService
service
=
new
DataService
(
dataRepository
)
;
service
.
getData
(
999L
)
;
service
.
getData
(
999L
)
;
// Should call repository twice because null results are not cached
verify
(
dataRepository
,
times
(
2
)
)
.
findById
(
999L
)
;
}
@Test
void
shouldNotCacheWhenConditionIsFalse
(
)
{
UserRepository
userRepository
=
mock
(
UserRepository
.
class
)
;
User
user
=
new
User
(
1L
,
"Alice"
)
;
when
(
userRepository
.
findById
(
-
1L
)
)
.
thenReturn
(
Optional
.
of
(
user
)
)
;
DataService
service
=
new
DataService
(
null
)
;
service
.
getUser
(
-
1L
)
;
service
.
getUser
(
-
1L
)
;
// Should call repository twice because id <= 0 doesn't match condition
verify
(
userRepository
,
times
(
2
)
)
.
findById
(
-
1L
)
;
}
}
Testing Cache Keys
Verify Cache Key Generation
@Service
public
class
InventoryService
{
private
final
InventoryRepository
inventoryRepository
;
public
InventoryService
(
InventoryRepository
inventoryRepository
)
{
this
.
inventoryRepository
=
inventoryRepository
;
}
@Cacheable
(
value
=
"inventory"
,
key
=
"#productId + '-' + #warehouseId"
)
public
InventoryItem
getInventory
(
Long
productId
,
Long
warehouseId
)
{
return
inventoryRepository
.
findByProductAndWarehouse
(
productId
,
warehouseId
)
;
}
}
class
CacheKeyTest
{
@Test
void
shouldGenerateCorrectCacheKey
(
)
{
InventoryRepository
repository
=
mock
(
InventoryRepository
.
class
)
;
InventoryItem
item
=
new
InventoryItem
(
1L
,
1L
,
100
)
;
when
(
repository
.
findByProductAndWarehouse
(
1L
,
1L
)
)
.
thenReturn
(
item
)
;
InventoryService
service
=
new
InventoryService
(
repository
)
;
service
.
getInventory
(
1L
,
1L
)
;
// Cache: "1-1"
service
.
getInventory
(
1L
,
1L
)
;
// Hit cache: "1-1"
service
.
getInventory
(
2L
,
1L
)
;
// Miss cache: "2-1"
verify
(
repository
,
times
(
2
)
)
.
findByProductAndWarehouse
(
any
(
)
,
any
(
)
)
;
}
}
Best Practices
Use in-memory CacheManager
for unit tests
Verify repository calls
to confirm cache hits/misses
Test both positive and negative
cache scenarios
Test cache invalidation
thoroughly
Test conditional caching
with various conditions
Keep cache configuration simple
in tests
Mock dependencies
that services use
Common Pitfalls
Testing actual cache infrastructure instead of caching logic
Not verifying repository call counts
Forgetting to test cache eviction
Not testing conditional caching
Not resetting cache between tests
Constraints and Warnings
@Cacheable requires a proxy
Spring's caching works via proxies; direct method calls bypass caching
Cache key collisions
Be aware that different parameters can produce the same cache key if key generation is not specific
Serialization requirements
Cached objects must be serializable when using distributed caches
Memory usage
In-memory caches can consume significant memory; consider TTL and max-size settings
@CachePut vs @Cacheable
@CachePut always executes the method, while @Cacheable skips execution on cache hit
Null caching
By default, null results are cached unless
unless = "#result == null"
is specified
Thread safety
Cache operations should be thread-safe; verify behavior under concurrent access
Troubleshooting
Cache not working in tests
Ensure
@EnableCaching
is in test configuration.
Wrong cache key generated
Use
SpEL
syntax correctly in
@Cacheable(key = "...")
.
Cache not evicting
Verify
@CacheEvict
key matches stored key exactly.
References
Spring Caching Documentation
Spring Cache Abstractions
SpEL in Caching
← 返回排行榜