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

前端 未结 2 523
情深已故
情深已故 2020-11-28 06:29

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

相关标签:
2条回答
  • 2020-11-28 07:12

    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
      }
    } 
    
    0 讨论(0)
  • 2020-11-28 07:13

    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.
    0 讨论(0)
提交回复
热议问题