unit-test-caching

Unit Testing Spring Caching

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "unit-test-caching" with this command: npx skills add giuseppe-trisciuoglio/developer-kit/giuseppe-trisciuoglio-developer-kit-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 &#x3C;= 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

shadcn-ui

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

tailwind-css-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

unit-test-bean-validation

No summary provided by upstream source.

Repository SourceNeeds Review