Как протестировать декларативное кэширование Spring в хранилищах данных Spring?

Я разработал репозиторий Spring, интерфейс MemberRepository, который расширяет org.springframework.data.jpa.repository.JpaRepository. MemberRepository имеет метод:

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

Результат кэшируется Spring абстракцией кэша (с поддержкой ConcurrentMapCache).

Проблема заключается в том, что я хочу написать тест интеграции (против hsqldb), который утверждает, что результат извлечен из db в первый раз и из кэша во второй раз.

Первоначально я думал об издевательстве инфраструктуры jpa (диспетчер сущностей и т.д.) и как-то утверждать, что диспетчер сущностей не называется вторым, но кажется слишком сложным/громоздким (см. qaru.site/info/86621/...).

Может ли кто-нибудь, пожалуйста, дать советы о том, как проверить поведение кэширования метода репозитория данных Spring, аннотированного с помощью @Cacheable?

Ответ 1

Если вы хотите протестировать технический аспект, такой как кеширование, вообще не используйте базу данных. Важно понять, что вы хотели бы проверить здесь. Вы хотите убедиться, что для вызова с помощью тех же самых аргументов исключается вызов метода. Репозиторий, выходящий из базы данных, является полностью ортогональным аспектом этой темы.

Вот что я бы рекомендовал:

  • Настройте тест интеграции, который настраивает декларативное кэширование (или импортирует необходимый бит и фрагменты из вашей производственной конфигурации.
  • Настройте экземпляр mock вашего репозитория.
  • Напишите тестовый пример для настройки ожидаемого поведения макета, вызовите методы и проверьте результат соответственно.

Пример

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

Как вы можете видеть, здесь мы немного переутомляем:

  • Самая важная проверка, я думаю, что второй вызов возвращает первый объект. Это то, что все кэширование. Первые два вызова с одним и тем же ключом возвращают один и тот же объект, тогда как третий вызов с другим ключом приводит к второму действительному вызову в репозитории.
  • Мы усиливаем тестовый пример, проверяя, что кеш имеет значение для первого ключа. Можно даже расширить это, чтобы проверить фактическое значение. С другой стороны, я также считаю, что это нормально, чтобы избежать этого, поскольку вы склонны тестировать больше внутренних компонентов механизма, а не поведение на уровне приложений.

Прицепы для ключей

  • Вам не нужна инфраструктура для проверки поведения контейнера.
  • Настройка тестового окна проста и проста.
  • Хорошо разработанные компоненты позволяют писать простые тестовые примеры и требуют меньше усилий для тестирования интеграции.

Ответ 2

Я попытался проверить поведение кэша в своем приложении, используя пример Оливера. В моем случае мой кеш установлен на уровне службы, и я хочу проверить, что мое репо называется правильным числом раз. Я использую 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
  }
}