Come testare il supporto di cache dichiarativa di Spring sui repository Spring Data?

Ho sviluppato un repository Spring Data, l’interfaccia MemberRepository , che estende org.springframework.data.jpa.repository.JpaRepository . MemberRepository ha un metodo:

 @Cacheable(CacheConfiguration.DATABASE_CACHE_NAME) Member findByEmail(String email); 

Il risultato è memorizzato nella cache da Spring cache astraction (supportato da ConcurrentMapCache ).

Il problema che ho è che voglio scrivere un test di integrazione (contro hsqldb) che asserisce che il risultato viene recuperato da db la prima volta e dalla cache la seconda volta .

Inizialmente pensavo di prendere in giro l’infrastruttura jpa (gestore di quadro, ecc.) E in qualche modo affermare che il gestore di entity framework non viene chiamato la seconda volta ma sembra troppo difficile / ingombrante (vedere https://stackoverflow.com/a/23442457/536299 ).

Qualcuno può quindi fornire un consiglio su come testare il comportamento di caching di un metodo Spring Data Repository annotato con @Cacheable ?

Se si desidera testare un aspetto tecnico come la memorizzazione nella cache, non utilizzare affatto un database. È importante capire cosa vorresti testare qui. Volete assicurarvi che l’invocazione del metodo sia evitata per l’invocazione con gli stessi argomenti. Il repository che fronteggia un database è un aspetto completamente ortogonale di questo argomento.

Ecco cosa consiglierei:

  1. Impostare un test di integrazione che configura la memorizzazione nella cache dichiarativa (o importa i bit e i pezzi necessari dalla configurazione di produzione.
  2. Configura un’istanza fittizia del tuo repository.
  3. Scrivi un caso di test per impostare il comportamento previsto della simulazione, richiamare i metodi e verificare l’output di conseguenza.

Campione

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class CachingIntegrationTest { // Your repository interface interface MyRepo extends Repository { @Cacheable("sample") Object findByEmail(String email); } @Configuration @EnableCaching static class Config { // Simulating your caching configuration @Bean CacheManager cacheManager() { return new ConcurrentMapCacheManager("sample"); } // A repository mock instead of the real proxy @Bean MyRepo myRepo() { return Mockito.mock(MyRepo.class); } } @Autowired CacheManager manager; @Autowired MyRepo repo; @Test public void methodInvocationShouldBeCached() { Object first = new Object(); Object second = new Object(); // Set up the mock to return *different* objects for the first and second call Mockito.when(repo.findByEmail(Mockito.any(String.class))).thenReturn(first, second); // First invocation returns object returned by the method Object result = repo.findByEmail("foo"); assertThat(result, is(first)); // Second invocation should return cached value, *not* second (as set up above) result = repo.findByEmail("foo"); assertThat(result, is(first)); // Verify repository method was invoked once Mockito.verify(repo, Mockito.times(1)).findByEmail("foo"); assertThat(manager.getCache("sample").get("foo"), is(notNullValue())); // Third invocation with different key is triggers the second invocation of the repo method result = repo.findByEmail("bar"); assertThat(result, is(second)); } } 

Come puoi vedere, facciamo un po ‘di prove eccessive qui:

  1. Il controllo più rilevante, penso sia che la seconda chiamata restituisca il primo object. Questo è tutto ciò che riguarda il caching. Le prime due chiamate con la stessa chiave restituiscono lo stesso object, mentre la terza chiamata con una chiave diversa genera la seconda chiamata effettiva sul repository.
  2. Rafforziamo il caso verificando che la cache abbia effettivamente un valore per la prima chiave. Si potrebbe anche estenderlo per verificare il valore effettivo. D’altra parte, penso anche che sia meglio evitare di farlo mentre tendi a testare più parti interne del meccanismo piuttosto che il comportamento a livello di applicazione.

Key take-away

  1. Non è necessario disporre di infrastrutture per testare il comportamento del contenitore.
  2. Impostare un caso di test è facile e diretto.
  3. I componenti ben progettati consentono di scrivere semplici casi di test e richiedono meno lavoro di integrazione per il test.

Ho provato a testare il comportamento della cache nella mia app utilizzando l’esempio di Oliver. Nel mio caso la mia cache è impostata sul livello di servizio e voglio verificare che il mio repo venga chiamato il numero giusto di volte. Sto usando spock mock invece di mockito. Ho passato un po ‘di tempo a cercare di capire perché i miei test stessero fallendo, fino a quando ho capito che i test eseguiti per primi stanno popolando la cache ed effettuando gli altri test. Dopo aver svuotato la cache per ogni test, hanno iniziato a comportarsi come previsto.

Ecco cosa ho finito con:

 @ContextConfiguration class FooBarServiceCacheTest extends Specification { @TestConfiguration @EnableCaching static class Config { def mockFactory = new DetachedMockFactory() def fooBarRepository = mockFactory.Mock(FooBarRepository) @Bean CacheManager cacheManager() { new ConcurrentMapCacheManager(FOOBARS) } @Bean FooBarRepository fooBarRepository() { fooBarRepository } @Bean FooBarService getFooBarService() { new FooBarService(fooBarRepository) } } @Autowired @Subject FooBarService fooBarService @Autowired FooBarRepository fooBarRepository @Autowired CacheManager cacheManager def "setup"(){ // we want to start each test with an new cache cacheManager.getCache(FOOBARS).clear() } def "should return cached foobars "() { given: final foobars = [new FooBar(), new FooBar()] when: fooBarService.getFooBars() fooBarService.getFooBars() final fooBars = fooBarService.getFooBars() then: 1 * fooBarRepository.findAll() >> foobars } def "should return new foobars after clearing cache"() { given: final foobars = [new FooBar(), new FooBar()] when: fooBarService.getFooBars() fooBarService.clearCache() final fooBars = fooBarService.getFooBars() then: 2 * fooBarRepository.findAll() >> foobars } }