web-dev-qa-db-fra.com

Comment tester la prise en charge de la mise en cache déclarative de Spring sur les référentiels Spring Data?

J'ai développé un référentiel Spring Data, MemberRepository interface, qui étend org.springframework.data.jpa.repository.JpaRepository. MemberRepository a une méthode:

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

Le résultat est mis en cache par l'abstraction du cache Spring (soutenu par un ConcurrentMapCache).

Le problème que j'ai est que je veux écrire un test d'intégration (contre hsqldb) qui affirme que le résultat est récupéré de db la première fois et à partir du cache pour la deuxième fois .

J'ai d'abord pensé à se moquer de l'infrastructure jpa (gestionnaire d'entité, etc.) et j'affirme en quelque sorte que le gestionnaire d'entité n'est pas appelé la deuxième fois, mais cela semble trop difficile/lourd (voir https://stackoverflow.com/a/23442457/536299 ).

Quelqu'un peut-il alors fournir des conseils sur la façon de tester le comportement de mise en cache d'une méthode Spring Data Repository annotée avec @Cacheable?

28
balteo

Si vous souhaitez tester un aspect technique comme la mise en cache, n'utilisez pas du tout de base de données. Il est important de comprendre ce que vous souhaitez tester ici. Vous voulez vous assurer que l'invocation de la méthode est évitée pour l'invocation avec les mêmes arguments. Le référentiel devant une base de données est un aspect complètement orthogonal à ce sujet.

Voici ce que je recommanderais:

  1. Configurez un test d'intégration qui configure la mise en cache déclarative (ou importe les bits et les morceaux nécessaires de votre configuration de production.
  2. Configurez une instance fictive de votre référentiel.
  3. Écrivez un scénario de test pour configurer le comportement attendu de la maquette, appelez les méthodes et vérifiez la sortie en conséquence.

Échantillon

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class CachingIntegrationTest {

  // Your repository interface
  interface MyRepo extends Repository<Object, Long> {

    @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));
  }
}

Comme vous pouvez le voir, nous faisons un peu de sur-test ici:

  1. La vérification la plus pertinente, je pense, est que le deuxième appel renvoie le premier objet. C'est à cela que sert la mise en cache. Les deux premiers appels avec la même clé renvoient le même objet, tandis que le troisième appel avec une clé différente entraîne le deuxième appel réel sur le référentiel.
  2. Nous renforçons le cas de test en vérifiant que le cache a réellement une valeur pour la première clé. On pourrait même étendre cela pour vérifier la valeur réelle. D'un autre côté, je pense également que c'est bien d'éviter de le faire car vous avez tendance à tester davantage les composants internes du mécanisme plutôt que le comportement au niveau de l'application.

Points clés à retenir

  1. Vous n'avez besoin d'aucune infrastructure pour mettre en place le comportement du conteneur.
  2. La configuration d'un scénario de test est simple et directe.
  3. Des composants bien conçus vous permettent d'écrire des cas de test simples et nécessitent moins de travail d'intégration pour les tests.
58
Oliver Drotbohm

J'ai essayé de tester le comportement du cache dans mon application en utilisant l'exemple d'Oliver. Dans mon cas, mon cache est défini sur la couche de service et je veux vérifier que mon dépôt est appelé le bon nombre de fois. J'utilise des spock mocks au lieu de mockito. J'ai passé un certain temps à essayer de comprendre pourquoi mes tests échouent, jusqu'à ce que je réalise que les tests en cours d'exécution remplissent le cache et effectuent les autres tests. Après avoir vidé le cache pour chaque test, ils ont commencé à se comporter comme prévu.

Voici ce que j'ai fini avec:

@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
  }
} 
1
Mustafa