Comment tester les référentiels Spring Data?

Je veux un référentiel (disons UserRepository ) créé à l’aide de Spring Data. Je suis nouveau à Spring-data (mais pas au spring) et j’utilise ce tutoriel . Mon choix de technologies pour gérer la firebase database est JPA 2.1 et Hibernate. Le problème est que je ne sais pas comment écrire des tests unitaires pour un tel référentiel.

Prenons par exemple la méthode create() . Pendant que je travaille en test, je suis censé écrire un test unitaire – et c’est là que je rencontre trois problèmes:

  • Tout d’abord, comment puis-je injecter une maquette d’un EntityManager dans l’implémentation non existante d’une interface UserRepository ? Spring Data génère une implémentation basée sur cette interface:

     public interface UserRepository extends CrudRepository {} 

    Cependant, je ne sais pas comment le forcer à utiliser un simulacre EntityManager et d’autres simulacres – si j’avais écrit l’implémentation moi-même, j’aurais probablement une méthode setter pour EntityManager , me permettant d’utiliser mon simulacre pour le test unitaire. (En ce qui concerne la connectivité de la firebase database, j’ai une classe JpaConfiguration , annotée avec @Configuration et @EnableJpaRepositories , qui définit par programmation les beans pour DataSource , @EnableJpaRepositories , @EnableJpaRepositories , etc.).

  • Deuxièmement, devrais-je tester les interactions? Il est difficile pour moi de savoir quelles méthodes d’ EntityManager et de Query sont supposées être appelées (similaire à la verify(entityManager).createNamedQuery(anySsortingng()).getResultList(); ), puisque ce n’est pas moi qui écrit la mise en oeuvre.

  • Troisièmement, suis-je censé tester en premier lieu les méthodes générées par Spring-Data? Comme je le sais, le code de la bibliothèque tierce n’est pas censé être testé par l’unité – seul le code que les développeurs écrivent eux-mêmes est censé être testé par l’unité. Mais si c’est vrai, cela ramène toujours la première question à la scène: disons que j’ai quelques méthodes personnalisées pour mon repository, pour lesquelles j’écrirai une implémentation, comment puis-je injecter mes simulacres d’ EntityManager et de Query dans la finale? , référentiel généré?

Remarque: je testerai mes référentiels en utilisant à la fois les tests d’intégration et les tests unitaires. Pour mes tests d’intégration, j’utilise une firebase database en mémoire HSQL, et je n’utilise évidemment pas de firebase database pour les tests unitaires.

Et probablement la quasortingème question, est-il correct de tester la création correcte du graphe d’object et la récupération du graphe d’object dans les tests d’intégration (disons que j’ai un graphe d’object complexe défini avec Hibernate)?

Mise à jour: aujourd’hui, j’ai continué à expérimenter avec l’injection simulée – j’ai créé une classe interne statique pour permettre l’injection simulée.

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @Transactional @TransactionConfiguration(defaultRollback = true) public class UserRepositoryTest { @Configuration @EnableJpaRepositories(basePackages = "com.anything.repository") static class TestConfiguration { @Bean public EntityManagerFactory entityManagerFactory() { return mock(EntityManagerFactory.class); } @Bean public EntityManager entityManager() { EntityManager entityManagerMock = mock(EntityManager.class); //when(entityManagerMock.getMetamodel()).thenReturn(mock(Metamodel.class)); when(entityManagerMock.getMetamodel()).thenReturn(mock(MetamodelImpl.class)); return entityManagerMock; } @Bean public PlatformTransactionManager transactionManager() { return mock(JpaTransactionManager.class); } } @Autowired private UserRepository userRepository; @Autowired private EntityManager entityManager; @Test public void shouldSaveUser() { User user = new UserBuilder().build(); userRepository.save(user); verify(entityManager.createNamedQuery(anySsortingng()).executeUpdate()); } } 

Cependant, l’exécution de ce test me donne le stacktrace suivant:

 java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:99) at org.springframework.test.context.DefaultTestContext.getApplicationContext(DefaultTestContext.java:101) at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:109) at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:75) at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:319) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:212) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:232) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:89) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71) at org.junit.runners.ParentRunner.run(ParentRunner.java:309) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:175) at org.junit.runner.JUnitCore.run(JUnitCore.java:160) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:77) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:195) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:63) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120) Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository': Error setting property values; nested exception is org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are: PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'entityManager' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null! at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1493) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1197) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:684) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482) at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:121) at org.springframework.test.context.support.AbstractGenericContextLoader.loadContext(AbstractGenericContextLoader.java:60) at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.delegateLoading(AbstractDelegatingSmartContextLoader.java:100) at org.springframework.test.context.support.AbstractDelegatingSmartContextLoader.loadContext(AbstractDelegatingSmartContextLoader.java:250) at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContextInternal(CacheAwareContextLoaderDelegate.java:64) at org.springframework.test.context.CacheAwareContextLoaderDelegate.loadContext(CacheAwareContextLoaderDelegate.java:91) ... 28 more Caused by: org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are: PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'entityManager' threw exception; nested exception is java.lang.IllegalArgumentException: JPA Metamodel must not be null! at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:108) at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:62) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1489) ... 44 more 

tl; dr

Pour faire court – il n’y a aucun moyen de tester les repositorys de Spring Data JPA raisonnablement pour une raison simple: il est difficile de simuler toutes les parties de l’API JPA que nous appelons pour amorcer les référentiels. Les tests unitaires n’ont pas beaucoup de sens ici, car vous n’écrivez pas vous-même de code d’implémentation (voir le paragraphe ci-dessous sur les implémentations personnalisées) afin que le test d’intégration soit l’approche la plus raisonnable.

Détails

Nous faisons beaucoup de validation et de configuration en amont pour vous assurer que vous ne pouvez démarrer qu’une application qui ne contient pas de requêtes dérivées non valides, etc.

  • Nous créons et mettons en cache des instances CriteriaQuery pour les requêtes dérivées afin de nous assurer que les méthodes de requête ne contiennent aucune faute de frappe. Cela nécessite de travailler avec l’API Criteria ainsi qu’avec le méta.model.
  • Nous vérifions les requêtes définies manuellement en demandant à EntityManager de créer une instance de Query pour ceux-ci (ce qui déclenche efficacement la validation de la syntaxe des requêtes).
  • Nous inspectons le Metamodel pour les méta-données sur les types de domaine traités pour préparer les nouvelles vérifications, etc.

Tout ce que vous voudriez probablement reporter dans un référentiel écrit à la main, qui pourrait provoquer la rupture de l’application à l’exécution (à cause de requêtes non valides, etc.).

Si vous y réfléchissez, il n’y a pas de code que vous écrivez pour vos référentiels, il n’est donc pas nécessaire d’écrire des tests unitaires . Il n’est tout simplement pas nécessaire que vous puissiez compter sur notre base de test pour détecter les bogues de base (si vous en rencontrez toujours un, n’hésitez pas à créer un ticket ). Toutefois, les tests d’intégration doivent absolument tester deux aspects de votre couche de persistance, car ce sont les aspects liés à votre domaine:

  • cartographie des entités
  • sémantique de la requête (la syntaxe est vérifiée à chaque tentative de démarrage).

Tests d’intégration

Cela se fait généralement en utilisant une firebase database en mémoire et des scénarios de test qui amorcent Spring ApplicationContext généralement via la structure de contexte de test (comme vous le faites déjà), en pré-remplissant la firebase database (en insérant des instances via EntityManager ou repo). un fichier SQL simple), puis exécutez les méthodes de requête pour en vérifier le résultat.

Test d’implémentations personnalisées

Les parties d’implémentation personnalisées du référentiel sont écrites de manière à ne pas avoir à connaître JPA Spring Data. Ce sont des haricots de spring simples qui génèrent un EntityManager injecté. Vous voudrez peut-être essayer de vous moquer des interactions, mais pour être honnête, le test unitaire de la JPA n’a pas été une expérience trop agréable pour nous et fonctionne avec beaucoup d’indirections ( EntityManager -> CriteriaBuilder , CriteriaQuery etc. .) pour que vous vous retrouviez avec des simulacres renvoyant des moquettes, etc.

Avec Spring Boot + Spring Data, il est devenu assez facile:

 @RunWith(SpringRunner.class) @DataJpaTest public class MyRepositoryTest { @Autowired MyRepository subject; @Test public void myTest() throws Exception { subject.save(new MyEntity()); } } 

La solution de @heez apporte le contexte complet, ce qui ne fait qu’expliquer ce qui est nécessaire pour que JPA + Transaction fonctionne. Notez que la solution ci-dessus affichera une firebase database de test en mémoire étant donné que celle-ci peut être trouvée sur le classpath.

Cela peut arriver un peu tard, mais j’ai écrit quelque chose à cette fin. Ma bibliothèque va simuler les méthodes de base du référentiel brut et interpréter la plupart des fonctionnalités de vos méthodes de requête. Vous devrez injecter des fonctionnalités pour vos propres requêtes natives, mais le rest est fait pour vous.

Regarde:

https://github.com/mmnaseri/spring-data-mock

METTRE À JOUR

Ceci est maintenant dans le centre de Maven et en assez bon état.

Si vous utilisez Spring Boot, vous pouvez simplement utiliser @SpringBootTest pour charger votre ApplicationContext (ce que votre stacktrace vous lance). Cela vous permet de vous connecter automatiquement à vos référentiels de données de spring. Veillez à append @RunWith(SpringRunner.class) pour que les annotations spécifiques au spring soient sockets en compte:

 @RunWith(SpringRunner.class) @SpringBootTest public class OrphanManagementTest { @Autowired private UserRepository userRepository; @Test public void saveTest() { User user = new User("Tom"); userRepository.save(user); Assert.assertNotNull(userRepository.findOne("Tom")); } } 

Vous pouvez en savoir plus sur les tests au démarrage du spring dans leurs documents .

Avec JUnit5 et @DataJpaTest test ressemblera à (code kotlin):

 @DataJpaTest @ExtendWith(value = [SpringExtension::class]) class ActivityJpaTest { @Autowired lateinit var entityManager: TestEntityManager @Autowired lateinit var myEntityRepository: MyEntityRepository @Test fun shouldSaveEntity() { // when val savedEntity = myEntityRepository.save(MyEntity(1, "test") // then Assertions.assertNotNull(entityManager.find(MyEntity::class.java, savedEntity.id)) } } 

Vous pouvez utiliser TestEntityManager partir org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager package org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager afin de valider l’état de l’entité.

J’ai résolu ce problème en utilisant cette méthode –

  @RunWith(SpringRunner.class) @EnableJpaRepositories(basePackages={"com.path.repositories"}) @EntityScan(basePackages={"com.model"}) @TestPropertySource("classpath:application.properties") @ContextConfiguration(classes = {ApiTestConfig.class,SaveActionsServiceImpl.class}) public class SaveCriticalProcedureTest { @Autowired private SaveActionsService saveActionsService; ....... ....... }