Search code examples
javaspringspring-cache

Spring caching works "live" but not in tests


I have a service at work using Spring framework (not Boot!) with a guava cache. I would like to use Spring's own facilities. I can get it to work running the actual app, but in tests the caching doesn't work.

I've recreated the issue in a "minimal" app, see below.

I haven't found anything useful by googling, as far as I can tell I have all the ingredients that all the tutorials say to have.

Would appreciate some help with this. "Use Spring Boot" is unfortunately not a solution/option.

Source code, gradle and dir structure follow: (pom available on request)

Test:

package com.somepkg;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
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.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
//@ComponentScan("com.somepkg")
@ContextConfiguration(classes = TestConfig.class)
public class SomeTest {
    @Autowired MyApp myApp;

    @Test
    public void testStuff() {
        long start, stop, totalTime;

        start = System.currentTimeMillis();
        myApp.run("stuff");
        stop = System.currentTimeMillis();
        totalTime = stop - start;
        System.out.println("Total: " + totalTime);

        assertThat("Call took too long", totalTime < 1100);
    }
}

@Configuration
@EnableCaching
class TestConfig {
    @Bean MyApp myApp() { return new MyApp(new MyService()); }
    @Bean CacheManager cacheManager() { return new ConcurrentMapCacheManager("greeting"); }
}

App:

package com.somepkg;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.stereotype.Component;

@Configuration
@ComponentScan
@EnableCaching
public class Main {
    public static void main(String[] args) {
        try (GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(Main.class)) {
            MyApp app = applicationContext.getBean(MyApp.class);
            app.run("Spring");
        }
    }

    @Bean CacheManager cacheManager() { return new ConcurrentMapCacheManager("greeting"); }
}

@Component
class MyApp {
    private final MyService myService;

    public MyApp(MyService myService) { this.myService = myService; }

    public void run(String message) {
        long start, stop, totalTime;

        System.out.println("First call to service...");
        start = System.currentTimeMillis();
        myService.hello(message);
        stop = System.currentTimeMillis();
        totalTime = stop - start;

        System.out.println("Second call to service...");
        start = System.currentTimeMillis();
        myService.hello(message);
        stop = System.currentTimeMillis();
        totalTime += stop - start;

        System.out.println("done in " + totalTime);
    }
}

@Component
class MyService {
    @Cacheable("greeting")
    public String hello(String message) {
        try { Thread.sleep(1000); } catch (Exception e) {}
        return "Hello " + message + "!";
    }
}

build.gradle:

mainClassName = 'com.somepkg.Main'

run { standardInput = System.in }

repositories { jcenter() }

dependencies {
    implementation 'org.springframework:spring-context:5.3.17'
    implementation 'org.springframework:spring-test:5.3.17'
    implementation 'junit:junit:4.13.1'
    implementation 'org.hamcrest:hamcrest:2.2'
}

Directory structure:

├── build.gradle
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── somepkg
    │               └── Main.java
    └── test
        └── java
            └── com
                └── somepkg
                    └── SomeTest.java

Solution

  • The problem is that you are constructing the instance of type MyService by hand:

    @Bean MyApp myApp() { return new MyApp(new MyService()); }
    

    You need to let do spring the magic of creating all the beans. This way the @Cacheable annotations etc are applied via proxy. Since you are constructing the MyService by hand via new MyService(), you go exactly around spring for this single dependency, but this dependency needs the proxying of the methods for the @Cacheable annotation to work