Hands‑On Unit Tests for Full Spring Cache Features

This guide walks through setting up a Spring Boot 3.x project with JDK 17+, adding the necessary Maven dependencies, and implementing a comprehensive JUnit 5 test suite that demonstrates @Cacheable, @CachePut, @CacheEvict (single‑key and all‑entries), SpEL‑based keys, condition/unless caching, and the AOP proxy pitfall of same‑class calls, plus instructions for running the tests via Maven or IntelliJ IDEA.

CodeSmart Hoops
CodeSmart Hoops
CodeSmart Hoops
Hands‑On Unit Tests for Full Spring Cache Features

Version and Environment

All test code targets Spring Boot 3.x + Spring Framework 6.x running on JDK 17+ . The test framework used is JUnit 5 with Spring Boot Test .

Environment Configuration

Maven Dependencies (pom.xml)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.4</version>
</parent>
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- Spring Boot AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- Spring Boot Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Test Class

CacheTest – Full Cache Test Suite

The test class is ordered with @TestMethodOrder(MethodOrderer.OrderAnnotation.class) so that tests run sequentially.

package com.example.springcontainer.cache;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;

/**
 * Spring Cache annotation full test.
 * Covers @Cacheable, @CachePut, @CacheEvict, SpEL keys, condition/unless, and AOP proxy issues.
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@DisplayName("Spring Cache Full Test")
class CacheTest {
    @Autowired
    private CachedUserService cachedUserService;

    // 1. @Cacheable – cache hit
    @Test @Order(1) @DisplayName("Test 1: @Cacheable – first call stores, second call hits cache")
    void cacheableCachesResult() {
        Map<String, Object> user1 = cachedUserService.getUserById(1);
        assertNotNull(user1);
        assertEquals("Alice", user1.get("name"));
        Map<String, Object> user2 = cachedUserService.getUserById(1);
        assertSame(user1, user2, "Second call should hit cache and return same object");
        System.out.println("Test 1 passed: @Cacheable works");
    }

    // 2. @CachePut – cache update
    @Test @Order(2) @DisplayName("Test 2: @CachePut – method executes then updates cache")
    void cachePutUpdatesCache() {
        cachedUserService.getUserById(2); // populate null entry
        Map<String, Object> updated = cachedUserService.updateUser(1, "Alice-Updated", "[email protected]");
        assertNotNull(updated);
        assertEquals("Alice-Updated", updated.get("name"));
        Map<String, Object> user = cachedUserService.getUserById(1);
        assertEquals("Alice-Updated", user.get("name"), "@CachePut should update cache");
        System.out.println("Test 2 passed: @CachePut updates cache");
    }

    // 3. @CacheEvict – key delete
    @Test @Order(3) @DisplayName("Test 3: @CacheEvict – delete by key")
    void cacheEvictRemovesCache() {
        Map<String, Object> user = cachedUserService.getUserById(3);
        cachedUserService.deleteUser(3);
        Map<String, Object> after = cachedUserService.getUserById(3);
        assertNull(after);
        System.out.println("Test 3 passed: @CacheEvict key delete works");
    }

    // 4. @CacheEvict allEntries – clear all
    @Test @Order(4) @DisplayName("Test 4: @CacheEvict allEntries=true – clear all cache")
    void cacheEvictAll() {
        cachedUserService.getUserById(1);
        cachedUserService.getUserById(2);
        cachedUserService.clearAllCache();
        Map<String, Object> user1 = cachedUserService.getUserById(1);
        assertNotNull(user1);
        System.out.println("Test 4 passed: allEntries clear works");
    }

    // 5. SpEL key expressions
    @Test @Order(5) @DisplayName("Test 5: SpEL key expressions")
    void spelExpression() {
        Map<String, Object> u1 = cachedUserService.getUserById(1);
        Map<String, Object> u1Again = cachedUserService.getUserById(1);
        assertSame(u1, u1Again, "#id key should cache");
        Map<String, Object> query = Map.of("id", 1, "name", "ignored");
        Map<String, Object> r1 = cachedUserService.getUserByObject(query);
        Map<String, Object> r2 = cachedUserService.getUserByObject(query);
        assertSame(r1, r2, "#user['id'] key should cache");
        MyUser myUser = new MyUser(1, null, null);
        MyUser mu1 = cachedUserService.getUser(myUser);
        MyUser mu2 = cachedUserService.getUser(myUser);
        assertSame(mu1, mu2, "#myUser.id key should cache");
        Map<String, Object> m1 = cachedUserService.getUserByIdWithMethod(1);
        Map<String, Object> m2 = cachedUserService.getUserByIdWithMethod(1);
        assertSame(m1, m2, "#root.methodName key should cache");
        System.out.println("Test 5 passed: SpEL keys work");
    }

    // 6. condition – cache only when id > 0
    @Test @Order(6) @DisplayName("Test 6: condition cache – id > 0 caches")
    void conditionCache() {
        Map<String, Object> p1 = cachedUserService.getUserWithCondition(1);
        Map<String, Object> p1Again = cachedUserService.getUserWithCondition(1);
        assertSame(p1, p1Again, "id > 0 should cache");
        Map<String, Object> n1 = cachedUserService.getUserWithCondition(-1);
        Map<String, Object> n2 = cachedUserService.getUserWithCondition(-1);
        assertNull(n1);
        assertNull(n2);
        System.out.println("Test 6 passed: condition works");
    }

    // 7. unless – do not cache when result is null
    @Test @Order(7) @DisplayName("Test 7: unless – null result not cached")
    void unlessNotCache() {
        Map<String, Object> u1 = cachedUserService.getUserWithUnless(1);
        Map<String, Object> u1Again = cachedUserService.getUserWithUnless(1);
        assertSame(u1, u1Again, "Non‑null result should cache");
        Map<String, Object> miss1 = cachedUserService.getUserWithUnless(999);
        Map<String, Object> miss2 = cachedUserService.getUserWithUnless(999);
        assertNull(miss1);
        assertNull(miss2);
        System.out.println("Test 7 passed: unless works");
    }

    // 8. Same‑class call – AOP proxy issue
    @Test @Order(8) @DisplayName("Test 8: Same‑class call – proxy bypass")
    void sameClassCallCacheInvalid() {
        Map<String, Object> d1 = cachedUserService.getUserById(1);
        Map<String, Object> d2 = cachedUserService.getUserById(1);
        assertSame(d1, d2, "Proxy call should hit cache");
        Map<String, Object> i1 = cachedUserService.internalCall(1);
        Map<String, Object> i2 = cachedUserService.internalCall(1);
        assertSame(i1, i2, "Internal call bypasses proxy, cache not applied");
        System.out.println("Test 8 passed: same‑class proxy issue demonstrated");
    }
}

Result Screenshot:

Test result screenshot
Test result screenshot

Auxiliary Classes

Cache Configuration

package com.example.springcontainer.cache;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

/** Enable Spring Cache support */
@Configuration
@EnableCaching
public class CacheConfig {
}

Cache Service (business logic)

package com.example.springcontainer.cache;

import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/** Demonstrates all Spring Cache annotation scenarios */
@Service
public class CachedUserService {
    private final Map<Integer, Map<String, Object>> users = new ConcurrentHashMap<>();
    private final Map<Integer, MyUser> myUsers = new ConcurrentHashMap<>();
    private final AtomicInteger idGenerator = new AtomicInteger(0);

    { // preset data
        users.put(1, new ConcurrentHashMap<>(Map.of("id", 1, "name", "Alice", "email", "[email protected]")));
        myUsers.put(1, new MyUser(1, "Alice", "[email protected]"));
        myUsers.put(2, new MyUser(2, "Bob", "[email protected]"));
        myUsers.put(3, new MyUser(3, "Charlie", "[email protected]"));
    }

    @Cacheable(value = "users", key = "#id")
    public Map<String, Object> getUserById(int id) {
        System.out.println("[CachedUserService] actual query, id=" + id);
        return users.get(id);
    }

    @CachePut(value = "users", key = "#id")
    public Map<String, Object> updateUser(int id, String name, String email) {
        System.out.println("[CachedUserService] update user, id=" + id);
        Map<String, Object> user = users.get(id);
        if (user != null) {
            user.put("name", name);
            user.put("email", email);
        }
        return user;
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(int id) {
        System.out.println("[CachedUserService] delete user, id=" + id);
        users.remove(id);
    }

    @CacheEvict(value = "users", allEntries = true)
    public void clearAllCache() {
        System.out.println("[CachedUserService] clear all cache");
    }

    @Cacheable(value = "users", key = "#user['id']")
    public Map<String, Object> getUserByObject(Map<String, Object> user) {
        System.out.println("[CachedUserService] query with object, id=" + user.get("id"));
        int id = (int) user.get("id");
        return users.get(id);
    }

    @Cacheable(value = "myUsers", key = "#myUser.id")
    public MyUser getUser(MyUser myUser) {
        System.out.println("[CachedUserService] query MyUser, id=" + myUser.getId());
        return myUsers.get(myUser.getId());
    }

    @Cacheable(value = "users", key = "#root.methodName + '_' + #id")
    public Map<String, Object> getUserByIdWithMethod(int id) {
        System.out.println("[CachedUserService] method‑name key, id=" + id);
        return users.get(id);
    }

    @Cacheable(value = "users", key = "#id", condition = "#id > 0")
    public Map<String, Object> getUserWithCondition(int id) {
        System.out.println("[CachedUserService] condition query, id=" + id);
        return users.get(id);
    }

    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public Map<String, Object> getUserWithUnless(int id) {
        System.out.println("[CachedUserService] unless query, id=" + id);
        return users.get(id);
    }

    public Map<String, Object> internalCall(int id) {
        System.out.println("[CachedUserService] internal call, id=" + id);
        return this.getUserById(id); // bypasses proxy
    }

    @Cacheable(value = "users", key = "#id")
    private Map<String, Object> privateCacheMethod(int id) {
        System.out.println("[CachedUserService] private method, id=" + id);
        return users.get(id);
    }

    public Map<String, Object> callPrivateCacheMethod(int id) {
        return this.privateCacheMethod(id);
    }
}

Entity Class

package com.example.springcontainer.cache;

public class MyUser {
    private Integer id;
    private String name;
    private String email;

    public MyUser(Integer id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

Running the Tests

Method 1: Maven Command

# Run a single test class
mvn test -Dtest=CacheTest

Method 2: IntelliJ IDEA

Open any test class.

Right‑click the class name or a test method.

Select “Run ‘XXX’” or “Debug ‘XXX’”.

Related Documentation

Spring Cache Full Analysis – detailed cache‑mechanism documentation.

Applicable versions: Spring Boot 3.x + Spring Framework 6.x + JDK 17+

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavacacheSpringunit-testingSpring BootJUnit5
CodeSmart Hoops
Written by

CodeSmart Hoops

A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.