如何测试Spring对Spring Data存储库的声明性缓存支持?

时间:2014-06-14 15:41:52

标签: spring testing spring-data spring-data-jpa spring-cache

我开发了一个Spring MemberRepository接口,扩展了org.springframework.data.jpa.repository.JpaRepositoryMemberRepository有一个方法:

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

结果由Spring缓存抽象缓存(由ConcurrentMapCache支持)。

我遇到的问题是我想编写一个集成测试(针对hsqldb),该测试断言第二次从第二次从检索 强>

我最初想过嘲笑jpa基础架构(实体管理器等),并以某种方式断言第二次不调用实体管理器,但它似乎太难/繁琐(参见https://stackoverflow.com/a/23442457/536299)。

那么有人可以就如何测试使用@Cacheable注释的Spring Data Repository方法的缓存行为提供建议吗?

2 个答案:

答案 0 :(得分:52)

如果要测试缓存等技术方面,请不要使用数据库。了解您要在此测试的内容非常重要。您希望确保使用相同的参数调用方法调用。面向数据库的存储库与该主题完全正交。

以下是我的建议:

  1. 设置集成测试,配置声明性缓存(或从生产配置中导入必要的位和部分。
  2. 配置存储库的模拟实例。
  3. 编写测试用例以设置模拟的预期行为,调用方法并相应地验证输出。
  4. 样品

    @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));
      }
    }
    

    正如您所看到的,我们在这里做了一些过度测试:

    1. 最相关的检查,我认为是第二个调用返回第一个对象。这就是缓存的全部意义所在。使用相同键的前两个调用返回相同的对象,而使用不同键的第三个调用导致存储库上的第二个实际调用。
    2. 我们通过检查缓存实际上是否具有第一个键的值来加强测试用例。甚至可以扩展它以检查实际值。另一方面,我也认为可以避免这样做,因为你倾向于测试更多机制的内部而不是应用程序级别的行为。
    3. 关键外卖

      1. 您不需要任何基础设施来测试容器行为。
      2. 简单直接地设置测试用例。
      3. 精心设计的组件使您可以编写简单的测试用例,并且需要较少的集成支持来进行测试。

答案 1 :(得分:1)

我尝试使用Oliver的示例在我的应用中测试缓存行为。在我的情况下,我的缓存设置在服务层,我想验证我的repo被调用正确的次数。我正在使用spock mocks而不是mockito。我花了一些时间试图找出我的测试失败的原因,直到我意识到首先运行的测试填充缓存并影响其他测试。清除每个测试的缓存后,他们开始按预期运行。

这是我最终的结果:

@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
  }
}