Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test Spring's declarative caching support on Spring Data repositories?

I have developed a Spring Data repository, MemberRepository interface, that extends org.springframework.data.jpa.repository.JpaRepository. MemberRepository has a method:

@Cacheable(CacheConfiguration.DATABASE_CACHE_NAME) Member findByEmail(String email); 

The result is cached by Spring cache abstraction (backed by a ConcurrentMapCache).

The issue I have is that I want to write an integration test (against hsqldb) that asserts that the result is retrieved from db the first time and from cache the second time.

I initially thought of mocking the jpa infrastructure (entity manager, etc.) and somehow assert that the entity manager is not called the second time but it seems too hard/cumbersome (see https://stackoverflow.com/a/23442457/536299).

Can someone then please provide advice as to how to test the caching behavior of a Spring Data Repository method annotated with @Cacheable?

like image 330
balteo Avatar asked Jun 14 '14 15:06

balteo


People also ask

How do you test a cacheable method?

The Spring framework provides a simple way to cache the value returned by a method. Basically, you annotate a method with @Cacheable and next time you call it with the same arguments, you get the cached value avoiding re-executing the method's code.


1 Answers

If you want to test a technical aspect like caching, don't use a database at all. It's important to understand what you'd like to test here. You want to make sure the method invocation is avoided for the invocation with the very same arguments. The repository fronting a database is a completely orthogonal aspect to this topic.

Here's what I'd recommend:

  1. Set up an integration test that configures declarative caching (or imports the necessary bit's and pieces from your production configuration.
  2. Configure a mock instance of your repository.
  3. Write a test case to set up the expected behavior of the mock, invoke the methods and verify the output accordingly.

Sample

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class CachingIntegrationTest {    // Your repository interface   interface MyRepo extends Repository<Object, Long> {      @Cacheable("sample")     Object findByEmail(String email);   }    @Configuration   @EnableCaching   static class Config {      // Simulating your caching configuration     @Bean     CacheManager cacheManager() {       return new ConcurrentMapCacheManager("sample");     }      // A repository mock instead of the real proxy     @Bean     MyRepo myRepo() {       return Mockito.mock(MyRepo.class);     }   }    @Autowired CacheManager manager;   @Autowired MyRepo repo;    @Test   public void methodInvocationShouldBeCached() {      Object first = new Object();     Object second = new Object();      // Set up the mock to return *different* objects for the first and second call     Mockito.when(repo.findByEmail(Mockito.any(String.class))).thenReturn(first, second);      // First invocation returns object returned by the method     Object result = repo.findByEmail("foo");     assertThat(result, is(first));      // Second invocation should return cached value, *not* second (as set up above)     result = repo.findByEmail("foo");     assertThat(result, is(first));      // Verify repository method was invoked once     Mockito.verify(repo, Mockito.times(1)).findByEmail("foo");     assertThat(manager.getCache("sample").get("foo"), is(notNullValue()));      // Third invocation with different key is triggers the second invocation of the repo method     result = repo.findByEmail("bar");     assertThat(result, is(second));   } } 

As you can see, we do a bit of over-testing here:

  1. The most relevant check, I think is that the second call returns the first object. That's what the caching is all about. The first two calls with the same key return the same object, whereas the third call with a different key results in the second actual invocation on the repository.
  2. We strengthen the test case by checking that the cache actually has a value for the first key. One could even extend that to check for the actual value. On the other hand, I also think it's fine to avoid doing that as you tend to test more of the internals of the mechanism rather than the application level behavior.

Key take-aways

  1. You don't need any infrastructure to be in place to test container behavior.
  2. Setting a test case up is easy and straight forward.
  3. Well-designed components let you write simple test cases and require less integration leg work for testing.
like image 127
Oliver Drotbohm Avatar answered Sep 17 '22 02:09

Oliver Drotbohm