Home > database >  @Cacheable testing over method
@Cacheable testing over method

Time:04-06

I have a @Cacheable method inside a class. I try to create that cache after a first call to that method, then, the second call should't go inside the method getCacheLeads.

@Service
public class LeadService {

    @Autowired
    private LeadRepository leadRepository;

    @Autowired
    public LeadService(LeadRepository leadRepository) {
        this.leadRepository = leadRepository;
    }

    public void calculateLead(Lead leadBean) {
        Lead lead = this.getCacheLeads(leadBean);
    }

    @Cacheable(cacheNames="leads", key="#leadBean.leadId")
    public Lead getCacheLeads(Lead leadBean){
        Lead result = leadRepository.findByLeadId(leadBean.getLeadId());
        ***logic to transform de Lead object***
        return result;
    }
}

But during testing that cache is never used, calling it twice with same parameter (serviceIsCalled) to ensure it is called twice to check it.

@ExtendWith(SpringExtension.class)
public class LeadServiceTest {

    private LeadService leadService;
    
    @Mock
    private LeadRepository leadRepository;
    
    @Autowired 
    CacheManager cacheManager;
    
    @BeforeEach
    public void setUp(){
        leadService = new LeadService(leadRepository);
    }

    @Configuration
    @EnableCaching
    static class Config {
        @Bean
        CacheManager cacheManager() {
            return new ConcurrentMapCacheManager("leads"); 
        }
    }
    
    @Test
    public void testLead(){
        givenData();
        serviceIsCalled();
        serviceIsCalled();
        checkDataArray();
    }
    
    private void givenData() {
        Lead lead = new Lead();
        lead.setLeadId("DC635EA19A39EA128764BB99052E5D1A9A");
        
        Mockito.when(leadRepository.findByLeadId(any()))
        .thenReturn(lead);
    }
    
    private void serviceIsCalled(){
        Lead lead = new Lead();
        lead.setLeadId("DC635EA19A39EA128764BB99052E5D1A9A");
        leadService.calculateLead(lead);
    }
    
    private void checkDataArray(){
        verify(leadRepository, times(1)).findByLeadId(anyString());
    }
}

Why is it called 2 times?

CodePudding user response:

You have a lot of things going on here, and someone looking at this and answering your question would definitely have to read between the lines.

First, your Spring configuration is not even correct. You are declaring the names of all the caches used by your Spring application (and tests) "statically" with the use of the ConcurrentMapCacheManager constructor accepting an array of cache names as the argument.

NOTE: Caches identified explicitly by name, and only these caches, are available at runtime.

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

In this case, your 1 and only cache is called "LEAD_DATA".

NOTE: Only the no arg constructor in `ConcurrentMapCacheManager allows dynamically created caches by name at runtime.

But then, in your @Service LeadService class, @Cacheable getCacheLeads(:Lead) method, you declare the cache to use as "leads".

@Service
public class LeadService {

    @Cacheable(cacheNames="leads", key="#leadBean.leadId")
    public Lead getCacheLeads(Lead leadBean){
        // ...
    }
}

This miss configuration will actually lead to an Exception at runtime similar to the following:

java.lang.IllegalArgumentException: Cannot find cache named 'leads' for Builder[public io.stackoverflow.questions.spring.cache.StaticCacheNamesIntegrationTests$Lead io.stackoverflow.questions.spring.cache.StaticCacheNamesIntegrationTests$LeadService.load(io.stackoverflow.questions.spring.cache.StaticCacheNamesIntegrationTests$Lead)] caches=[leads] | key='#lead.id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'

    at org.springframework.cache.interceptor.AbstractCacheResolver.resolveCaches(AbstractCacheResolver.java:92)
    at org.springframework.cache.interceptor.CacheAspectSupport.getCaches(CacheAspectSupport.java:252)
    at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContext.<init>(CacheAspectSupport.java:724)
    at org.springframework.cache.interceptor.CacheAspectSupport.getOperationContext(CacheAspectSupport.java:265)
    at org.springframework.cache.interceptor.CacheAspectSupport$CacheOperationContexts.<init>(CacheAspectSupport.java:615)
    at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:345)
    at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:64)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
    at io.stackoverflow.questions.spring.cache.StaticCacheNamesIntegrationTests$LeadService$$EnhancerBySpringCGLIB$$86664246.load(<generated>)
...
..
.

Additionally, I don't see anything "outside" of the LeadsService bean calling the @Cacheable, getCacheLeads(..) method. Inside your test, you are calling:

leadService.calculateLead(lead);

As follows:

private void serviceIsCalled(){
    Lead lead = new Lead();
    lead.setLeadId("DC635EA19A39EA128764BB99052E5D1A9A");
    leadService.calculateLead(lead);
}

If the calculateLead(:Lead) LeadService method is calling the @Cacheable, getCacheLeads(:Lead) LeadService method (internally), then that is not going to cause the caching functionality to kick in since you are already "behind" the AOP Proxy setup by Spring to "enable" caching behavior for your LeadService bean.

See the Spring Framework AOP documentation on this matter.

NOTE: Spring's Cache Abstraction, like the Spring's Transaction Management, is built on the Spring AOP infrastructure, as are many other things in Spring.

In your case this means:

Test -> <PROXY> -> LeadService.calculateLead(:Lead) -> LeadService.getCacheLeads(:Lead)

However, between LeadSevice.calculateLead(:Lead) and LeadService.getCacheLeads(:Lead), NO PROXY is involved, therefore Spring's caching behavior will not be applied.

Only...

Test (or some other bean) -> <PROXY> -> LeadService.getCacheLeads(:Lead)

Will result in the AOP Proxy decorated with the Caching Interceptors being invoked and the caching behavior applied.

You can see that your use case will work correctly when configured and used correctly as demonstrated in my example test class, modeled after your domain.

Look for the comments that explain why your configuration will fail in your case.

  • Related