«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

Давайте начнем с простого теста 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. Plain Spring

Давайте теперь продолжим интеграционный тест 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.