【问题标题】:How to test Spring's declarative caching support on Spring Data repositories?如何在 Spring Data 存储库上测试 Spring 的声明式缓存支持?
【发布时间】:2014-08-04 23:20:10
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

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


    【解决方案1】:

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

    以下是我的建议:

    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. 精心设计的组件让您可以编写简单的测试用例,并且需要较少的集成测试工作。

    【讨论】:

    • 奥利弗:非常感谢您的详细回复。巧妙的策略!我不知道thenReturn 版本的可变参数版本...
    • 奥利弗:你说得对。今晚回家后,我将撤消更改并打开一个新帖子。
    • 您确定通过了吗?如果是这样,如果您显式调用此方法两次,Mockito.verify(repo, Mockito.times(1)).findByEmail("foo"); 这一行怎么可能通过?听起来像魔术。无论如何,在测试中你没有模拟引用,而是缓存引用。
    • 使用 Spring 4.0.5 和 Mockito 1.10.17,这只是部分工作。当我验证对模拟存储库(在我的情况下实际上是一个服务 bean)的调用时,无论我在 times() 中指定多少次,它总是通过。我还想在最后添加一个调用 1 次的验证,例如 verify(repo, Mockito.times(1)).findByEmail("bar"); 但这会产生一个奇怪的 Mockito UnfinishedVerificationException
    • @OliverGierke,如果您在对Mockito.verify() 的调用之后立即添加对Mockito.validateMockitoUsage() 的调用,您会看到问题。 Mockito 似乎无法与 Spring 生成的 repo 代理一起正常工作。
    【解决方案2】:

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

    【讨论】:

    • 谢谢,对我帮助很大!
    • @Mustafa 你能看看stackoverflow.com/questions/69270197/…吗?
    • @Mustafa Amigo,我们应该使用集成测试来测试缓存吗?或者我们也可以为此使用单元测试?
    • @Robert,请查看stackoverflow.com/questions/5357601/…,看看哪一个适用于您的测试。
    • @Mustafa 感谢 Amigo,但没有其他人使用我的测试。有什么想法吗?
    猜你喜欢
    • 2014-08-06
    • 2014-06-19
    • 1970-01-01
    • 2019-01-23
    • 1970-01-01
    • 2017-12-27
    • 2021-12-04
    • 1970-01-01
    • 2015-03-01
    相关资源
    最近更新 更多