Back to Testing & Quality Assurance

unit-test-caching

SpringCacheUnit TestJavaMockitoPerformanceBackend
282📄 MIT🕒 2026-06-15Source ↗

Install this skill

npx skills add giuseppe-trisciuoglio/developer-kit

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

Testing Spring cache abstractions efficiently requires decoupling application logic from heavy external storage like Redis or Memcached. This skill focuses on using the ConcurrentMapCacheManager to perform high-speed unit tests that validate cache-related annotations directly within a standard test runner. By substituting the cache backend with an in-memory map during the lifecycle of a test, developers can confirm that @Cacheable, @CacheEvict, and @CachePut behave as expected without spinning up integration environments. This approach verifies key generation logic, ensures methods execute only once for repeated inputs, and confirms that stale data is correctly purged after write operations. It provides a reliable feedback loop for developers managing complex caching policies, ensuring that performance optimizations do not introduce hidden data consistency bugs during the standard development lifecycle.

When to Use This Skill

  • Validating that a database repository is only queried once during multiple service calls
  • Confirming that deleting a record successfully removes it from the local cache
  • Testing complex conditional caching scenarios based on method arguments
  • Ensuring entire cache regions are cleared correctly after a bulk operation

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • how to unit test spring @Cacheable methods
  • validate spring cache eviction in unit tests
  • mock spring cache manager for testing
  • verify cache hit and miss in service layer test
  • test @Cacheable behavior without redis

Pro Tips

  • 💡Combine Mockito with an in-memory `ConcurrentMapCacheManager` to fully isolate caching tests without any external dependencies.
  • 💡Always assert not just the method's return value, but also the interaction count with the underlying service to confirm cache hits/misses.
  • 💡Pay close attention to cache key generation; create specific tests to ensure complex keys (e.g., using `keyGenerator` or SpEL) are working as expected.

What this skill does

  • Verify @Cacheable execution count via Mockito verification
  • Validate cache invalidation triggered by @CacheEvict
  • Confirm data synchronization for @CachePut operations
  • Isolate caching logic from infrastructure dependencies
  • Test custom cache key generation and conditional eviction

When not to use it

  • Testing network-level issues with distributed caches like Redis or Hazelcast
  • Validating cache TTL or eviction policies specific to external caching providers

Example workflow

  1. Configure a ConcurrentMapCacheManager within a test configuration class
  2. Mock the repository interface using Mockito to track interactions
  3. Execute the service method under test for the first time
  4. Trigger the secondary call to verify the cached value is returned
  5. Call the eviction or update method to reset the cache state
  6. Perform a final call to ensure the repository is accessed again

Prerequisites

  • spring-boot-starter-cache
  • mockito-core
  • assertj-core

Pitfalls & limitations

  • !Forgetting to use @EnableCaching in the test configuration context
  • !Confusing in-memory map behavior with distributed cache characteristics
  • !Mocking the service class itself instead of the underlying repository
  • !Failing to reset cache state between independent test methods

FAQ

Do I need a running Redis instance for these tests?
No, this approach uses ConcurrentMapCacheManager, which keeps everything in-memory for fast, dependency-free execution.
Why is my repository still being called after caching?
Check that your cache key generation is consistent and that the method being called is not invoked from within the same class, as Spring AOP proxies require external calls.
Can I test @CachePut with this method?
Yes, you can verify that the repository is updated and the cache state changes by inspecting the cache manager directly after the update call.

How it compares

Generic prompts often fail to suggest the correct local cache manager implementation, whereas this skill provides the specific infrastructure setup required to mock cache behavior without overhead.

Source & trust

282 stars📄 MIT🕒 Updated 2026-06-15
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
# Unit Testing Spring Caching

Test Spring caching annotations (@Cacheable, @CacheEvict, @CachePut) without full Spring context. Verify cache behavior, hits/misses, and invalidation strategies.

## When to Use This Skill

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

## 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

## 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](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache)
- [Spring Cache Abstractions](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/annotation/Cacheable.html)
- [SpEL in Caching](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions)

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/unit-test-caching/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-caching/SKILL.md
  • Cursor: ~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-caching/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-caching/SKILL.md

🚀 Install with CLI:
npx skills add giuseppe-trisciuoglio/developer-kit

Read the Master Guide: Mastering Agent Skills

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid testing & quality assurance issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Testing & Quality Assurance and is published by Giuseppe Trisciuoglio, maintained in giuseppe-trisciuoglio/developer-kit.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.