Agent skill
unit-test-caching
Provides patterns for unit testing Spring Cache annotations (@Cacheable, @CachePut, @CacheEvict). Generates test code that mocks cache managers, verifies cache hit/miss behavior, tests cache key generation with SpEL expressions, validates eviction strategies, and checks conditional caching scenarios. Triggers: caching tests, test Spring cache, mock cache, Spring Boot caching, cache hit/miss verification, @Cacheable testing.
Install this agent skill to your Project
npx add-skill https://github.com/giuseppe-trisciuoglio/developer-kit/tree/main/plugins/developer-kit-java/skills/unit-test-caching
SKILL.md
Unit Testing Spring Caching
Overview
This skill provides patterns for unit testing Spring caching annotations (@Cacheable, @CacheEvict, @CachePut) without full Spring context. It covers cache hits/misses, invalidation, key generation, and conditional caching using in-memory ConcurrentMapCacheManager.
When to Use
- Writing unit tests for
@Cacheablemethod behavior - Verifying
@CacheEvictcache invalidation works correctly - Testing
@CachePutcache updates - Validating cache key generation from SpEL expressions
- Testing conditional caching with
unless/conditionparameters - Mocking cache managers in fast unit tests without Redis
Instructions
- Configure in-memory CacheManager: Use
ConcurrentMapCacheManagerfor tests - Set up test fixtures: Mock repository and create service instance in
@BeforeEach - Verify repository call counts: Use
times(n)assertions to confirm cache behavior - Test cache hit: Call method twice, verify repository called once
- Test cache miss: Verify repository called on each invocation
- Test eviction: After
@CacheEvict, verify repository called again on next read - Test key generation: Verify compound keys from SpEL expressions
- Validate conditional caching: Test
unless(null results) andcondition(parameter-based)
Validation checkpoints:
- Run test → If cache not working: verify
@EnableCachingannotation present - If proxy issues: ensure method calls go through Spring proxy (no direct
thiscalls) - If key mismatches: log actual cache key and compare with
@Cacheable(key="...")expression
Examples
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>
Gradle
dependencies {
implementation("org.springframework.boot:spring-boot-starter-cache")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Testing @Cacheable (Cache Hit/Miss)
// Service
@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
class UserServiceCachingTest {
private UserRepository userRepository;
private UserService userService;
@BeforeEach
void setUp() {
userRepository = mock(UserRepository.class);
userService = new UserService(userRepository);
}
@Test
void shouldCacheUserAfterFirstCall() {
User user = new User(1L, "Alice");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
// First call - hits database
User firstCall = userService.getUserById(1L);
// Second call - hits cache
User secondCall = userService.getUserById(1L);
assertThat(firstCall).isEqualTo(secondCall);
verify(userRepository, times(1)).findById(1L); // Only once due to cache
}
@Test
void shouldInvokeRepositoryOnCacheMiss() {
when(userRepository.findById(1L)).thenReturn(Optional.of(new User(1L, "Bob")));
userService.getUserById(1L);
userService.getUserById(1L);
verify(userRepository, times(2)).findById(1L); // No caching occurred
}
}
Testing @CacheEvict
// Service
@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);
}
}
// Test
class ProductCacheEvictTest {
private ProductRepository productRepository;
private ProductService productService;
@BeforeEach
void setUp() {
productRepository = mock(ProductRepository.class);
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
// Repository called again after eviction
productService.getProductById(1L);
verify(productRepository, times(2)).findById(1L);
}
@Test
void shouldClearAllEntriesWithAllEntriesTrue() {
Product product1 = new Product(1L, "Laptop", 999.99);
Product product2 = new Product(2L, "Mouse", 29.99);
when(productRepository.findById(anyLong())).thenAnswer(i ->
Optional.of(new Product(i.getArgument(0), "Product", 10.0)));
productService.getProductById(1L);
productService.getProductById(2L);
// Use reflection or clear() on ConcurrentMapCache
productService.clearAllProducts();
productService.getProductById(1L);
productService.getProductById(2L);
verify(productRepository, times(4)).findById(anyLong());
}
}
Testing @CachePut
@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 original = new Order(1L, "Pending", 100.0);
Order updated = new Order(1L, "Shipped", 100.0);
when(orderRepository.findById(1L)).thenReturn(Optional.of(original));
when(orderRepository.save(updated)).thenReturn(updated);
orderService.getOrder(1L);
orderService.updateOrder(updated);
// Next call returns updated version from cache
Order cachedOrder = orderService.getOrder(1L);
assertThat(cachedOrder.getStatus()).isEqualTo("Shipped");
}
}
Testing Conditional Caching
@Service
public class DataService {
private final DataRepository dataRepository;
public DataService(DataRepository dataRepository) {
this.dataRepository = dataRepository;
}
// Don't cache null results
@Cacheable(value = "data", unless = "#result == null")
public Data getData(Long id) {
return dataRepository.findById(id).orElse(null);
}
// Only cache when id > 0
@Cacheable(value = "users", condition = "#id > 0")
public User getUser(Long id) {
return dataRepository.findById(id).map(u -> new User(u.getId(), u.getName())).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);
verify(dataRepository, times(2)).findById(999L); // Called twice - no caching
}
@Test
void shouldNotCacheWhenConditionIsFalse() {
DataRepository dataRepository = mock(DataRepository.class);
when(dataRepository.findById(-1L)).thenReturn(Optional.of(new Data(-1L, "Test")));
DataService service = new DataService(dataRepository);
service.getUser(-1L);
service.getUser(-1L);
verify(dataRepository, times(2)).findById(-1L); // Condition "#id > 0" = false
}
}
Testing Cache Keys with SpEL
@Service
public class InventoryService {
private final InventoryRepository inventoryRepository;
public InventoryService(InventoryRepository inventoryRepository) {
this.inventoryRepository = inventoryRepository;
}
// Compound key: productId-warehouseId
@Cacheable(value = "inventory", key = "#productId + '-' + #warehouseId")
public InventoryItem getInventory(Long productId, Long warehouseId) {
return inventoryRepository.findByProductAndWarehouse(productId, warehouseId);
}
}
class CacheKeyTest {
@Test
void shouldUseCorrectCacheKeyForDifferentCombinations() {
InventoryRepository repository = mock(InventoryRepository.class);
InventoryItem item = new InventoryItem(1L, 1L, 100);
when(repository.findByProductAndWarehouse(1L, 1L)).thenReturn(item);
InventoryService service = new InventoryService(repository);
// Same key: "1-1" - should cache
service.getInventory(1L, 1L);
service.getInventory(1L, 1L); // Cache hit
verify(repository, times(1)).findByProductAndWarehouse(1L, 1L);
// Different key: "2-1" - cache miss
service.getInventory(2L, 1L); // Cache miss
verify(repository, times(2)).findByProductAndWarehouse(any(), any());
}
}
Best Practices
- Mock repository calls: Use
verify(mock, times(n))to assert cache behavior - Test both hit and miss scenarios: Don't just test the happy path
- Clear cache state: Reset between tests to avoid flaky results
- Use
ConcurrentMapCacheManager: Fast, no external dependencies - Verify eviction: Always test that
@CacheEvictactually invalidates cached data
Constraints and Warnings
@Cacheablerequires proxy: Direct method calls (this.method()) bypass caching - use dependency injection- Cache key collisions: Compound keys from SpEL must be unique per dataset
- Null caching: Null results are cached by default - use
unless = "#result == null"to exclude @CachePutalways executes: Unlike@Cacheable, it always runs the method- Memory usage: In-memory caches grow unbounded - consider TTL for long-running tests
- Thread safety:
ConcurrentMapCacheManageris thread-safe; distributed caches may require additional config
Troubleshooting
| Issue | Solution |
|---|---|
| Cache not working | Verify @EnableCaching on test config |
| Proxy bypass | Use autowired/constructor injection, not direct this calls |
| Key mismatch | Log cache key with cache.getNativeKey() to debug SpEL |
| Flaky tests | Clear cache in @BeforeEach before each test |
References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
aws-cli-beast
Provides advanced AWS CLI patterns for managing EC2, Lambda, S3, DynamoDB, RDS, VPC, IAM, and CloudWatch. Generates bulk operation scripts, automates cross-service workflows, validates security configurations, and executes JMESPath queries for complex filtering. Triggers on "aws cli help", "aws command line", "aws scripting", "aws automation", "aws batch operations", "aws bulk operations", "aws cli pagination", "aws multi-region", "aws profiles", "aws cli troubleshooting".
aws-cost-optimization
Provides structured AWS cost optimization guidance using five pillars (right-sizing, elasticity, pricing models, storage optimization, monitoring) and twelve actionable best practices with executable AWS CLI examples. Use when optimizing AWS costs, reviewing AWS spending, finding unused AWS resources, implementing FinOps practices, reducing EC2/EBS/S3 bills, configuring AWS Budgets, or performing AWS Well-Architected cost reviews.
aws-sam-bootstrap
Provides AWS SAM bootstrap patterns: generates `template.yaml` and `samconfig.toml` for new projects via `sam init`, creates SAM templates for existing Lambda/CloudFormation code migration, validates build/package/deploy workflows, and configures local testing with `sam local invoke`. Use when the user asks about SAM projects, `sam init`, `sam deploy`, serverless deployments, or needs to bootstrap/migrate Lambda functions with SAM templates.
aws-drawio-architecture-diagrams
Creates professional AWS architecture diagrams in draw.io XML format (.drawio files) using official AWS Architecture Icons (aws4 library). Use when the user asks for AWS diagrams, VPC layouts, multi-tier architectures, serverless designs, network topology, or draw.io exports involving Lambda, EC2, RDS, or other AWS services.
aws-cloudformation-bedrock
Provides AWS CloudFormation patterns for Amazon Bedrock resources including agents, knowledge bases, data sources, guardrails, prompts, flows, and inference profiles. Use when creating Bedrock agents with action groups, implementing RAG with knowledge bases, configuring vector stores, setting up content moderation guardrails, managing prompts, orchestrating workflows with flows, and configuring inference profiles for model optimization.
aws-cloudformation-s3
Provides AWS CloudFormation patterns for Amazon S3. Use when creating S3 buckets, policies, versioning, lifecycle rules, and implementing template structure with Parameters, Outputs, Mappings, Conditions, and cross-stack references.
Didn't find tool you were looking for?