1. Overview
In addition to implementations, we can use Spring's declarative caching mechanism to annotate interfaces. For instance, we can declare caching on a Spring Data repository.
In this tutorial, we're going to show how to test such a scenario.
2. Getting Started
First, let's create a simple model:
@Entity public class Book { @Id private UUID id; private String title; }
And then, let's add a repository interface that has a @Cacheable method:
public interface BookRepository extends CrudRepository<Book, UUID> { @Cacheable(value = "books", unless = "#a0=='Foundation'") Optional<Book> findFirstByTitle(String title); }
The unless condition here is not mandatory. It will just help us test some cache-miss scenarios in a moment.
Also, note the SpEL expression “#a0” instead of the more readable “#title”. We do this because the proxy won't keep the parameter names. So, we use the alternative #root.arg[0], p0 or a0 notation.
3. Testing
The goal of our tests is to make sure the caching mechanism works. Therefore, we don't intend to cover the Spring Data repository implementation or the persistence aspects.
3.1. Spring Boot
Let's start with a simple Spring Boot test.
First, we'll set up our test dependencies, add some test data, and create a simple utility method to check whether a book is in the cache or not:
@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)); }
Now, let's make sure that after requesting a book, it gets placed in the cache:
@Test void givenBookThatShouldBeCached_whenFindByTitle_thenResultShouldBePutInCache() { Optional<Book> dune = repository.findFirstByTitle("Dune"); assertEquals(dune, getCachedBook("Dune")); }
And also, that some books are not placed in the cache:
@Test void givenBookThatShouldNotBeCached_whenFindByTitle_thenResultShouldNotBePutInCache() { repository.findFirstByTitle("Foundation"); assertEquals(empty(), getCachedBook("Foundation")); }
In this test, we make use of the Spring-provided CacheManager and check that after each repository.findFirstByTitle operation, the CacheManager contains (or does not contain) books according to the @Cacheable rules.
3.2. Plain Spring
Let's now continue with a Spring integration test. And for a change, this time let's mock our interface. Then we'll verify interactions with it in different test cases.
We'll start by creating a @Configuration that provides the mock implementation for our 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"); } }
Before moving on to setting up our mock's behavior, there are a two aspects worth mentioning about successfully using Mockito in this context:
- BookRepository is a proxy around our mock. So, in order to use Mockito validations, we retrieve the actual mock via AopTestUtils.getTargetObject
- We make sure to reset(mock) in between tests because CachingTestConfig loads only once
@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!")); }
Now, we can add our test methods. We'll start by making sure that after a book is placed in the cache, there are no more interactions with the repository implementation when later trying to retrieve that book:
@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); }
And we also want to check that for non-cached books, we invoke the repository every time:
@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. Summary
To sum it up, we used Spring, Mockito, and Spring Boot to implement a series of integration tests that make sure the caching mechanism applied to our interface works properly.
Note that we could also combine the approaches above. For example, nothing stops us from using mocks with Spring Boot or from performing checks on the CacheManager in the plain Spring test.
The complete code is available over on GitHub.