如何在Spring Data存储库上测试Spring的声明式缓存支持?

55

我开发了一个Spring Data仓库,MemberRepository接口,它继承了org.springframework.data.jpa.repository.JpaRepositoryMemberRepository有一个方法:

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

这个结果由Spring缓存抽象(由ConcurrentMapCache支持)进行缓存。

我的问题是,我想编写一个集成测试(针对hsqldb),用于断言第一次从数据库中检索结果,并从缓存中检索结果的第二次

我最初考虑模拟jpa基础设施(实体管理器等),并以某种方式断言第二次未调用实体管理器,但似乎太难/繁琐(请参见https://dev59.com/leo6XIcBkEYKwwoYJA3p#23442457)。

那么,有人能否提供关于如何测试带有@Cacheable注释的Spring Data Repository方法的缓存行为的建议?

2个回答

97

如果您想测试缓存等技术方面,根本不需要使用数据库。重要的是要理解您想在这里测试什么。您希望确保避免使用完全相同参数进行调用的方法调用。与此主题完全无关的是面向数据库的存储库。

以下是我的建议:

  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. 设计良好的组件可让您编写简单的测试用例,并减少测试期间集成工作的负担。

7
Oliver:非常感谢您详细的回复。不错的策略!我之前不知道thenReturn方法还有可变参数版本... - balteo
3
你确定这个测试会通过吗?如果它能够通过,那么如果你明确地调用了这个方法两次,为什么这一行代码 Mockito.verify(repo, Mockito.times(1)).findByEmail("foo"); 会通过呢?听起来像是魔法。不过,在测试中你并没有使用模拟引用,而是缓存引用。 - makasprzak
2
我已更改存储库上的注释为@Cacheable(value = "sample", key = "#email.toString()")当我运行测试时,出现以下错误:org.springframework.expression.spel.SpelEvaluationException: EL1011E:(pos 7): Method call: Attempted to call method toString() on null context object有任何想法为什么会出现这个错误或如何修复它吗? - wischan
7
使用 Spring 4.0.5 和 Mockito 1.10.17,这只有部分起作用。当我验证对模拟 repo 的调用时(实际上,在我的情况下是一个服务 bean),无论我在 times() 中指定多少次,它总是通过。我还想在最后添加一个调用一次的确认,如 verify(repo, Mockito.times(1)).findByEmail("bar");,但会导致奇怪的 Mockito UnfinishedVerificationException 异常。 - E-Riz
4
@OliverGierke,如果您在调用Mockito.verify()之后立即添加对Mockito.validateMockitoUsage()的调用,则可以看到问题。似乎Mockito与Spring生成的repo代理不兼容。 - E-Riz
显示剩余14条评论

3

我尝试使用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
  }
} 

谢谢,它帮了我很多! - Samoth
@Mustafa,你能看一下这个链接吗?https://dev59.com/-MLra4cB1Zd3GeqPDi3r - user16409822
@Mustafa Amigo,我们应该使用集成测试来测试缓存吗?还是我们也可以使用单元测试来进行测试? - Jack
@Robert,请查看https://dev59.com/ZW435IYBdhLWcg3wigok,看看哪个适用于您的测试。 - Mustafa
@Mustafa谢谢朋友,但是没有其他人使用我的测试。任何想法? - Jack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接