1

I have my cache config as below;

@Configuration
public class CacheConfiguration {

    @Bean
    public CacheManager cacheManager(Ticker ticker) {
        CaffeineCache bookCache = buildCache("books", ticker, 30);
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Collections.singletonList(bookCache));
        return cacheManager;
    }

    private CaffeineCache buildCache(String name, Ticker ticker, int minutesToExpire) {
        return new CaffeineCache(name, Caffeine.newBuilder()
                .expireAfterWrite(minutesToExpire, TimeUnit.MINUTES)
                .maximumSize(100)
                .ticker(ticker)
                .build());
    }

    @Bean
    public Ticker ticker() {
        return Ticker.systemTicker();
    }
}

And the service I want to test:

@Service
public class TestServiceImpl implements TestService {

    private final BookRepository bookRepository; // interface

    @Autowired
    public TestServiceImpl(final BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @Override
    public Book getByIsbn(String isbn) {
        return bookRepository.getByIsbn(isbn);
    }
}

The required method in repository is annotated with @Cacheable("books").

@Override
@Cacheable("books")
public Book getByIsbn(String isbn) {
    LOGGER.info("Fetching Book...");
    simulateSlowService(); //  Wait for 5 secs
    return new Book(isbn, "Some book");
}

I need to write a test showing the caching works. So I created another ticker bean in test to override the one existing in CacheConfiguration. The code;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestServiceTests {

    private static final String BOOK_ISBN = "isbn-8442";

    @SpyBean
    private BookRepository bookRepository;

    @Autowired
    private TestService testService;

    @Configuration
    @Import(SpringBootCacheApplication.class)
    public static class TestConfiguration {

        //testCompile('com.google.guava:guava-testlib:23.6-jre')
        static FakeTicker fakeTicker = new FakeTicker();

        @Bean
        public Ticker ticker() {
            return fakeTicker::read;
        }
    }

    @Before
    public void setUp() {
        Book book = fakeBook();
        doReturn(book)
                .when(bookRepository)
                .getByIsbn(BOOK_ISBN);
    }

    private Book fakeBook() {
        return new Book(BOOK_ISBN, "Mock Book");
    }

    @Test
    public void shouldUseCache() {
        // Start At 0 Minutes
        testService.getByIsbn(BOOK_ISBN);
        verify(bookRepository, times(1)).getByIsbn(BOOK_ISBN);

        // After 5 minutes from start, it should use cached object
        TestConfiguration.fakeTicker.advance(5, TimeUnit.MINUTES);
        testService.getByIsbn(BOOK_ISBN);
        verify(bookRepository, times(1)).getByIsbn(BOOK_ISBN); // FAILS HERE

        // After 35 Minutes from start, it should call the method again
        TestConfiguration.fakeTicker.advance(30, TimeUnit.MINUTES);
        testService.getByIsbn(BOOK_ISBN);
        verify(bookRepository, times(2)).getByIsbn(BOOK_ISBN);
    }
}

But it fails at the line marked with //FAILS HERE with message;

org.mockito.exceptions.verification.TooManyActualInvocations:
simpleBookRepository.getByIsbn("isbn-8442");
Wanted 1 time:
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
But was 2 times. Undesired invocation:
-> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)`

Why it fails? Shouldn't it use cache? Or my test is wrong?

Any help or pointers are greatly appreciated! :)

aSemy
  • 5,485
  • 2
  • 25
  • 51
Rajkishan Swami
  • 3,569
  • 10
  • 48
  • 68
  • 2
    (Not a Spring user) - The `BookRepository` is spied on to return a value rather than call the delegate. Since the cache is on the repository, it should be called twice but intercepted by the spy instead of invoking the bean. Try promoting the cache to the service-level. – Ben Manes Dec 29 '17 at 19:47
  • Moved the cache to service level and now the test passes. I completely ignored the fact that, the spy intercepts the calls to the bean. Please post this as an answer, so that i can mark the question solved :) Thank you! – Rajkishan Swami Jan 02 '18 at 08:26

1 Answers1

0
verify(bookRepository, times(1)).getByIsbn(BOOK_ISBN); // FAILS HERE

Ofcourse it fails here. because ~4 lines before you already called one times this method. In this check you should put times(2). And on the next checking number of invocations should be times(3)

borino
  • 1,720
  • 1
  • 16
  • 24