48

我开发了一个 Spring Data 存储库MemberRepository接口,它扩展了org.springframework.data.jpa.repository.JpaRepository. MemberRepository有一个方法:

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

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

我遇到的问题是我想编写一个集成测试(针对 hsqldb),断言结果是第一次从 db 中检索,第二次从缓存中检索。

我最初想嘲笑 jpa 基础设施(实体管理器等),并以某种方式断言实体管理器没有被第二次调用,但它似乎太难/太麻烦了(参见https://stackoverflow.com/a/23442457/536299)。

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

4

2 回答 2

85

如果您想测试缓存等技术方面,请不要使用数据库。了解您想在这里测试什么很重要。您要确保使用相同参数的调用避免方法调用。面向数据库的存储库与本主题完全正交。

这是我的建议:

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

样本

@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. 我们通过检查缓存是否确实具有第一个键的值来加强测试用例。甚至可以扩展它以检查实际值。另一方面,我也认为最好避免这样做,因为您倾向于测试更多机制的内部而不是应用程序级别的行为。

关键要点

  1. 您不需要任何基础设施来测试容器行为。
  2. 设置测试用例既简单又直接。
  3. 精心设计的组件让您可以编写简单的测试用例,并且需要较少的集成测试工作。
于 2014-06-15T11:55:57.373 回答
3

我尝试使用 Oliver 的示例测试我的应用程序中的缓存行为。在我的情况下,我的缓存设置在服务层,我想验证我的 repo 被调用的次数是否正确。我正在使用 spock 模拟而不是 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
  }
} 
于 2018-01-26T05:08:57.897 回答