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

2

56

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://mcmap.net/q/134773/-how-to-test-spring-data-repositories).

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

Quasimodo answered 14/6, 2014 at 15:41 Comment(0)
A
99

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.
Assumptive answered 15/6, 2014 at 11:55 Comment(19)
Oliver: Thanks very much for this detailed reply. Neat strategy! I was not aware of the varargs version of the thenReturn version...Quasimodo
Hi again. I was to quick at claiming victory. I ran into issues when I tried to adapt your solution to my app. I have edited my post.Quasimodo
No sure we do the question a favor if we bend it with implementation details (and now effectively have multiple questions in there), as now all of a sudden the answer looks somewhat inappropriate (as if I didn't read the question correctly). I suggest to undo the changes and then create a new question with detailed questions regarding the implementation. To be honest I don't quite get what you're running into and it would be helpful if you could add more details on that in the new question.Assumptive
Oliver: You're right. I will undo the changes and open a new post when I return home tonight.Quasimodo
@OliverGierke Done: I have opened a new question here: https://mcmap.net/q/752783/-issues-with-testing-spring-39-s-declarative-caching-support-on-a-spring-data-repository/536299Quasimodo
Are you sure this passes? If it does, how is it possible that this line Mockito.verify(repo, Mockito.times(1)).findByEmail("foo"); passes if you explicitely invoke this method twice? Sounds like magic. Anyway, in the test you don't have the mock reference but the cache reference.Fadge
@macias - You're getting a proxy injected that has the caching advice applied. This one will invoke the mock for the first call but exactly not do that for the second one (as we invoke it with the same arguments). The second actual call to the mock happens once we change the argument as the caching advice doesn't find a cached value for this one then.Assumptive
@OliverGierke thanks for explanation. I was suspecting this might be a kind of such trick, haven't chance to try it out. Although as reported by balteo here https://mcmap.net/q/752784/-mockito-throwing-unfinishedverificationexception-probably-related-to-native-method-call/1937263, mockito doesn't really like that trick;) It fails the validation of mockito usageFadge
I changed the annotation on the repository to @Cacheable(value = "sample", key = "#email.toString()") When I run the test i get org.springframework.expression.spel.SpelEvaluationException: EL1011E:(pos 7): Method call: Attempted to call method toString() on null context object Any Idea why or how to fix it?Xanthate
Using Spring 4.0.5 and Mockito 1.10.17, this is only partially working. When I verify calls to the mocked repo (actually a service bean, in my case), it doesn't matter how many times I specify in times(), it always passes. I also wanted to add a verification of the invocation called 1 times at the end, like verify(repo, Mockito.times(1)).findByEmail("bar"); but that produces a strange Mockito UnfinishedVerificationException.Shirl
@OliverGierke, You can see the problem if you add a call to Mockito.validateMockitoUsage() right after the call to Mockito.verify(). It seems that Mockito does not work properly with the Spring-generated repo proxy.Shirl
@Shirl I had exactly the same problems in Spring Boot 1.5.9.RELEASE.Sadly, one of the most important part of the code is verify the number of the times that the method was called.Obelize
@Obelize I am also struggling with the same issue. Were you able to solve the issue?Funke
How can you handle same example, but with List return value? @Cacheable("sample") List<String> findByEmail(String email);Intersect
One improvement with more recent Spring Boot versions is using @TestConfiguration instead of @Configuration on the inner Config class.Occasionalism
Great answer Oliver. Do you know how to assert that value is in a cache when my cacheable method has more than one parameter? E.g.: @Cacheable("sample") Object findBy(String paramOne, String paramTwo); I do not know what is a cache key in such case. I there a way to access proxy to get "key" method?Syverson
@OliverDrotbohm Could you pls have a look at #69270697 ?Glidden
@OliverDrotbohm Chico, should we test caching via Integration Test? Or can we use Unit Test as well?Hind
@ChicoOliverDrotbohm Any example please for Caching Unit Test?Hind
D
4

I tried testing the cache behavior in my app using Oliver's example. In my case my cache is set at the service layer and I want to verify that my repo is being called the right number of times. I'm using spock mocks instead of mockito. I spent some time trying to figure out why my tests are failing, until I realized that tests running first are populating the cache and effecting the other tests. After clearing the cache for every test they started behaving as expected.

Here's what I ended up with:

@ContextConfiguration
class FooBarServiceCacheTest extends Specification {

  @TestConfiguration
  @EnableCaching
  static class Config {

    def mockFactory = new DetachedMockFactory()
    def fooBarRepository = mockFactory.Mock(FooBarRepository)

    @Bean
    CacheManager cacheManager() {
      new ConcurrentMapCacheManager(FOOBARS)
    }

    @Bean
    FooBarRepository fooBarRepository() {
      fooBarRepository
    }

    @Bean
    FooBarService getFooBarService() {
      new FooBarService(fooBarRepository)
    }
  }

  @Autowired
  @Subject
  FooBarService fooBarService

  @Autowired
  FooBarRepository fooBarRepository

  @Autowired
  CacheManager cacheManager

  def "setup"(){
    // we want to start each test with an new cache
    cacheManager.getCache(FOOBARS).clear()
  }

  def "should return cached foobars "() {

    given:
    final foobars = [new FooBar(), new FooBar()]

    when:
    fooBarService.getFooBars()
    fooBarService.getFooBars()
    final fooBars = fooBarService.getFooBars()

    then:
    1 * fooBarRepository.findAll() >> foobars
  }

def "should return new foobars after clearing cache"() {

    given:
    final foobars = [new FooBar(), new FooBar()]

    when:
    fooBarService.getFooBars()
    fooBarService.clearCache()
    final fooBars = fooBarService.getFooBars()

    then:
    2 * fooBarRepository.findAll() >> foobars
  }
} 
Decoteau answered 26/1, 2018 at 5:8 Comment(5)
Thanks, it helped me a lot!Greenbelt
@Decoteau Could you pls have a look at #69270697 ?Glidden
@Decoteau Amigo, should we use Integration Test for testing cache? Or can we also use Unit Testing for that?Hind
@Robert, please have a look at #5358101 and see which one applies to your test.Decoteau
@Decoteau Thanks Amigo, but no one else uses my test. Any idea?Hind

© 2022 - 2024 — McMap. All rights reserved.