Перейти к основному содержимому

Тестирование @Cacheable в репозиториях данных Spring

· 4 мин. чтения

1. Обзор

В дополнение к реализациям мы можем использовать механизм декларативного кэширования Spring для аннотирования интерфейсов . Например, мы можем объявить кэширование в репозитории Spring Data.

В этом уроке мы собираемся показать, как протестировать такой сценарий.

2. Начало работы

Сначала создадим простую модель:

@Entity
public class Book {

@Id
private UUID id;
private String title;

}

Затем добавим интерфейс репозитория с методом @Cacheable :

public interface BookRepository extends CrudRepository<Book, UUID> {

@Cacheable(value = "books", unless = "#a0=='Foundation'")
Optional<Book> findFirstByTitle(String title);

}

Условие « если » здесь не является обязательным. Это просто поможет нам протестировать некоторые сценарии промаха кеша через мгновение.

Также обратите внимание на выражение SpEL «#a0» вместо более читаемого «#title» . Мы делаем это, потому что прокси не будет хранить имена параметров. Итак, мы используем альтернативную нотацию #root.arg[0], p0 или a0 .

3. Тестирование

Цель наших тестов — убедиться, что механизм кэширования работает. Поэтому мы не собираемся рассматривать реализацию репозитория Spring Data или аспекты сохраняемости.

3.1. Весенний ботинок

Начнем с простого теста Spring Boot.

Во-первых, мы настроим наши тестовые зависимости, добавим некоторые тестовые данные и создадим простой служебный метод, чтобы проверить, находится ли книга в кеше или нет:

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = CacheApplication.class)
public class BookRepositoryIntegrationTest {

@Autowired
CacheManager cacheManager;

@Autowired
BookRepository repository;

@BeforeEach
void setUp() {
repository.save(new Book(UUID.randomUUID(), "Dune"));
repository.save(new Book(UUID.randomUUID(), "Foundation"));
}

private Optional<Book> getCachedBook(String title) {
return ofNullable(cacheManager.getCache("books")).map(c -> c.get(title, Book.class));
}

Теперь давайте удостоверимся , что после запроса книга помещается в кеш :

@Test
void givenBookThatShouldBeCached_whenFindByTitle_thenResultShouldBePutInCache() {
Optional<Book> dune = repository.findFirstByTitle("Dune");

assertEquals(dune, getCachedBook("Dune"));
}

А также, что некоторые книги не помещаются в тайник :

@Test
void givenBookThatShouldNotBeCached_whenFindByTitle_thenResultShouldNotBePutInCache() {
repository.findFirstByTitle("Foundation");

assertEquals(empty(), getCachedBook("Foundation"));
}

В этом тесте мы используем предоставленный Spring CacheManager и проверяем , что после каждой операции репозитория.findFirstByTitle CacheManager содержит (или не содержит) книги в соответствии с правилами @Cacheable .

3.2. Обычная весна

Давайте теперь продолжим интеграционный тест Spring. И для разнообразия, на этот раз давайте помокаем наш интерфейс. Затем мы проверим взаимодействие с ним в разных тест-кейсах.

Мы начнем с создания @Configuration , который обеспечивает фиктивную реализацию для нашего BookRepository :

@ContextConfiguration
@ExtendWith(SpringExtension.class)
public class BookRepositoryCachingIntegrationTest {

private static final Book DUNE = new Book(UUID.randomUUID(), "Dune");
private static final Book FOUNDATION = new Book(UUID.randomUUID(), "Foundation");

private BookRepository mock;

@Autowired
private BookRepository bookRepository;

@EnableCaching
@Configuration
public static class CachingTestConfig {

@Bean
public BookRepository bookRepositoryMockImplementation() {
return mock(BookRepository.class);
}

@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("books");
}

}

Прежде чем перейти к настройке поведения нашего макета, стоит упомянуть два аспекта успешного использования Mockito в этом контексте:

  • BookRepositoryэто прокси вокруг нашего мока. Итак, чтобы использовать проверки Mockito , мы получаем фактический макет через AopTestUtils.getTargetObject.
  • Мы обязательно сбрасываем (имитируем) между тестами, потому что CachingTestConfig загружается только один раз .
@BeforeEach
void setUp() {
mock = AopTestUtils.getTargetObject(bookRepository);

reset(mock);

when(mock.findFirstByTitle(eq("Foundation")))
.thenReturn(of(FOUNDATION));

when(mock.findFirstByTitle(eq("Dune")))
.thenReturn(of(DUNE))
.thenThrow(new RuntimeException("Book should be cached!"));
}

Теперь мы можем добавить наши методы тестирования. Мы начнем с того, что после помещения книги в кеш больше не будет взаимодействия с реализацией репозитория при последующей попытке получить эту книгу:

@Test
void givenCachedBook_whenFindByTitle_thenRepositoryShouldNotBeHit() {
assertEquals(of(DUNE), bookRepository.findFirstByTitle("Dune"));
verify(mock).findFirstByTitle("Dune");

assertEquals(of(DUNE), bookRepository.findFirstByTitle("Dune"));
assertEquals(of(DUNE), bookRepository.findFirstByTitle("Dune"));

verifyNoMoreInteractions(mock);
}

И мы также хотим проверить , что для некэшированных книг мы каждый раз вызываем репозиторий :

@Test
void givenNotCachedBook_whenFindByTitle_thenRepositoryShouldBeHit() {
assertEquals(of(FOUNDATION), bookRepository.findFirstByTitle("Foundation"));
assertEquals(of(FOUNDATION), bookRepository.findFirstByTitle("Foundation"));
assertEquals(of(FOUNDATION), bookRepository.findFirstByTitle("Foundation"));

verify(mock, times(3)).findFirstByTitle("Foundation");
}

4. Резюме

Подводя итог, мы использовали Spring, Mockito и Spring Boot для реализации серии интеграционных тестов, чтобы убедиться, что механизм кэширования, примененный к нашему интерфейсу, работает правильно.

Обратите внимание, что мы также можем комбинировать вышеперечисленные подходы. Например, ничто не мешает нам использовать моки с Spring Boot или выполнять проверки CacheManager в простом тесте Spring.

Полный код доступен на GitHub .