Persistence top

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:


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:

public class Book {

    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:

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

    CacheManager cacheManager;

    BookRepository repository;

    void setUp() { Book(UUID.randomUUID(), "Dune")); 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:

    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:

    void givenBookThatShouldNotBeCached_whenFindByTitle_thenResultShouldNotBePutInCache() {

        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:

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;

    private BookRepository bookRepository;

    public static class CachingTestConfig {

        public BookRepository bookRepositoryMockImplementation() {
            return mock(BookRepository.class);

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


Before moving on to setting up our mock's behavior, there are 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
    void setUp() {
        mock = AopTestUtils.getTargetObject(bookRepository);



                .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:

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

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


And we also want to check that for non-cached books, we invoke the repository every time:

    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.

Persistence bottom
Get started with Spring Data JPA through the reference Learn Spring Data JPA course: >> CHECK OUT THE COURSE
Comments are closed on this article!