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 .