53

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?

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
balteo
  • 23,602
  • 63
  • 219
  • 412

2 Answers2

94

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.
Brad Parks
  • 66,836
  • 64
  • 257
  • 336
Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 7
    Oliver: Thanks very much for this detailed reply. Neat strategy! I was not aware of the varargs version of the `thenReturn` version... – balteo Jun 15 '14 at 12:36
  • 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. – balteo Jun 15 '14 at 16:39
  • 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. – Oliver Drotbohm Jun 16 '14 at 09:00
  • 1
    Oliver: You're right. I will undo the changes and open a new post when I return home tonight. – balteo Jun 16 '14 at 10:33
  • @OliverGierke Done: I have opened a new question here: http://stackoverflow.com/q/24249873/536299 – balteo Jun 18 '14 at 09:08
  • 3
    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. – makasprzak Jun 29 '14 at 03:52
  • @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. – Oliver Drotbohm Jun 30 '14 at 05:34
  • @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 http://stackoverflow.com/q/24467675/1937263, mockito doesn't really like that trick;) It fails the validation of mockito usage – makasprzak Jun 30 '14 at 07:35
  • 2
    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? – wischan Oct 28 '15 at 15:12
  • 7
    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`. – E-Riz Nov 20 '15 at 15:51
  • 4
    @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. – E-Riz Nov 20 '15 at 16:46
  • 1
    @E-Riz 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. – Dherik Feb 28 '18 at 21:13
  • @Dherik I am also struggling with the same issue. Were you able to solve the issue? – Reena Upadhyay Oct 01 '18 at 06:27
  • How can you handle same example, but with List return value? @Cacheable("sample") List findByEmail(String email); – akasha Apr 11 '19 at 12:22
  • One improvement with more recent Spring Boot versions is using `@TestConfiguration` instead of `@Configuration` on the inner `Config` class. – Wim Deblauwe Sep 25 '20 at 07:25
  • 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? – Michal Foksa Dec 18 '20 at 08:01
  • @OliverDrotbohm Could you pls have a look at https://stackoverflow.com/questions/69270197/unit-test-for-redis-cache-in-java ? –  Sep 21 '21 at 14:58
  • @OliverDrotbohm Chico, should we test caching via Integration Test? Or can we use Unit Test as well? – Jack Sep 27 '21 at 07:33
  • @ChicoOliverDrotbohm Any example please for Caching Unit Test? – Jack Sep 27 '21 at 07:34
3

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
  }
} 
Mustafa
  • 5,624
  • 3
  • 24
  • 40
  • Thanks, it helped me a lot! – Samoth Apr 18 '18 at 13:45
  • @Mustafa Could you pls have a look at https://stackoverflow.com/questions/69270197/unit-test-for-redis-cache-in-java ? –  Sep 21 '21 at 14:58
  • @Mustafa Amigo, should we use Integration Test for testing cache? Or can we also use Unit Testing for that? – Jack Sep 27 '21 at 07:36
  • @Robert, please have a look at https://stackoverflow.com/questions/5357601/whats-the-difference-between-unit-tests-and-integration-tests and see which one applies to your test. – Mustafa Sep 27 '21 at 12:19
  • @Mustafa Thanks Amigo, but no one else uses my test. Any idea? – Jack Sep 27 '21 at 13:08