web-dev-qa-db-fra.com

Comment tester les référentiels Spring Data?

Je veux un référentiel (disons, UserRepository) créé avec l'aide de Spring Data. Je suis nouveau sur Spring-Data (mais pas sur Spring) et j'utilise ce tutorial . JOP 2.1 et Hibernate sont mon choix de technologies pour traiter la base de données. 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(). Comme je travaille d’abord à un test, je suis censé écrire un test unitaire - c’est là que je me heurte à trois problèmes:

  • Premièrement, comment puis-je injecter une maquette de EntityManager dans l'implémentation non existante d'une interface UserRepository? Spring Data générerait une implémentation basée sur cette interface:

    public interface UserRepository extends CrudRepository<User, Long> {}
    

    Cependant, je ne sais pas comment le forcer à utiliser une maquette EntityManager et d'autres simulacres. Si j'avais écrit l'implémentation moi-même, j'aurais probablement une méthode de définition pour EntityManager, me permettant d'utiliser ma maquette pour le test unitaire. (En ce qui concerne la connectivité réelle à la base de données, j'ai une classe JpaConfiguration, annotée avec @Configuration et @EnableJpaRepositories, qui définit par programme les beans pour DataSource, EntityManagerFactory, EntityManager etc. - mais les référentiels doivent être conviviaux pour les tests et permettre de remplacer ces éléments).

  • Deuxièmement, devrais-je tester les interactions? Il m'est difficile de déterminer quelles méthodes de EntityManager et Query sont censées être appelées (ce qui s'apparente à ce verify(entityManager).createNamedQuery(anyString()).getResultList();), car ce n'est pas moi qui rédige l'implémentation.

  • Troisièmement, suis-je censé tester en bloc 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 unité - seul le code que les développeurs écrivent est censé être testé par unité. Mais si cela est vrai, cela ramène toujours la première question à la scène: disons, j’ai quelques méthodes personnalisées pour mon référentiel, pour lesquelles je vais écrire l’implémentation, comment puis-je injecter mes simulacres de EntityManager et Query dans la version finale , référentiel généré?

Remarque: je vais tester mes référentiels en utilisant both l'intégration et les tests unitaires. Pour mes tests d'intégration, j'utilise une base de données HSQL en mémoire et je n'utilise évidemment pas de base de données pour les tests unitaires.

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

Mise à jour: aujourd'hui, j'ai continué à expérimenter 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(anyString()).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
101
user1797032

tl; dr

Pour résumer, il n’ya aucun moyen de tester les référentiels Spring Data JPA de manière raisonnable pour une raison simple: c’est trop lourd pour simuler toutes les parties de l’API JPA que nous invoquons pour amorcer les référentiels. De toute façon, les tests unitaires n’ont pas beaucoup de sens, car vous n’écrivez généralement pas vous-même de code d’implémentation (voir le paragraphe ci-dessous sur les implémentations personnalisées), de sorte que les tests d’intégration constituent l’approche la plus raisonnable.

Détails

Nous effectuons beaucoup de validation et de configuration initiales pour nous assurer que vous ne pouvez amorcer qu'une application ne contenant aucune requête dérivée invalide, 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 pas de fautes 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 à la EntityManager de créer une instance Query pour celles-ci (ce qui déclenche effectivement la validation de la syntaxe de la requête).
  • Nous examinons la Metamodel pour les métadonnées sur les types de domaine traités pour préparer les vérifications is-new, etc.

Tous les éléments que vous allez probablement différer dans un référentiel écrit à la main, susceptibles de provoquer la casse de l'application au moment de l'exécution (en raison de requêtes non valides, etc.).

Si vous y réfléchissez, vous n’écrivez pas de code pour vos référentiels, il n’est donc pas nécessaire d’écrire des tests unit. Inutile de le faire, car vous pouvez vous fier à notre base de tests pour détecter les bogues élémentaires (si vous en rencontrez toujours un, n'hésitez pas à envoyer un ticket ). Cependant, des tests d'intégration sont absolument nécessaires pour tester deux aspects de votre couche de persistance, car ce sont les aspects liés à votre domaine: 

  • mappages d'entités 
  • sémantique de la requête (la syntaxe est vérifiée à chaque tentative d'amorçage).

Tests d'intégration

Cela se fait généralement en utilisant une base de données en mémoire et des cas de test qui amorcent une variable Spring ApplicationContext généralement via le cadre de contexte de test (comme vous le faites déjà), pré-remplissez la base de données (en insérant des instances d'objet via la EntityManager ou le référentiel, ou via un fichier SQL simple), puis exécutez les méthodes de requête pour en vérifier le résultat.

Test des implémentations personnalisées

Les parties d'implémentation personnalisées du référentiel sont écrites en quelque sorte qu'elles ne doivent pas connaître sur Spring Data JPA. Ce sont des haricots de printemps ordinaires qui ont reçu une EntityManager injection. Vous pourriez bien sûr vouloir essayer de vous moquer des interactions avec cela, mais pour être honnête, le test unitaire de l’APP n’a pas été une expérience trop agréable pour nous, car il fonctionne avec beaucoup d’indices indirects (EntityManager -> CriteriaBuilder, CriteriaQuery etc. .) afin que vous vous retrouviez avec des répliques, et ainsi de suite.

88
Oliver Drotbohm

Avec Spring Boot + Spring Data, il est devenu très 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 affiche le contexte complet, elle n’apporte que ce qui est nécessaire au bon fonctionnement de JPA + Transaction . Notez que la solution ci-dessus fera apparaître une base de données de test en mémoire, étant donné que l’on peut en trouver une sur le chemin de classe. .

27
Markus T

Cela peut venir un peu trop tard, mais j’ai écrit quelque chose dans ce but précis. Ma bibliothèque simulera pour vous les méthodes de base du référentiel crud et interprétera la plupart des fonctionnalités de vos méthodes de requête .. Vous devrez insérer des fonctionnalités pour vos propres requêtes natives, mais le reste sera fait pour vous.

Regarde:

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

METTRE &AGRAVE; JOUR

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

19
Milad Naseri

Si vous utilisez Spring Boot, vous pouvez simplement utiliser @SpringBootTest pour charger votre ApplicationContext (c'est ce sur quoi votre trace de pile aboie). Cela vous permet de connecter automatiquement vos référentiels de données de printemps. Assurez-vous d'ajouter @RunWith(SpringRunner.class) pour que les annotations spécifiques aux ressorts soient relevées:

@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 dans Spring Boot dans leurs docs .

12
heez

J'ai résolu ceci en utilisant cette façon - 

    @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;
        .......
        .......
}
3
Ajay

Lorsque vous voulez vraiment écrire un i-test pour un référentiel de données Spring, vous pouvez le faire comme ceci:

@RunWith(SpringRunner.class)
@DataJpaTest
@EnableJpaRepositories(basePackageClasses = WebBookingRepository.class)
@EntityScan(basePackageClasses = WebBooking.class)
public class WebBookingRepositoryIntegrationTest {

    @Autowired
    private WebBookingRepository repository;

    @Test
    public void testSaveAndFindAll() {
        WebBooking webBooking = new WebBooking();
        webBooking.setUuid("some uuid");
        webBooking.setItems(Arrays.asList(new WebBookingItem()));
        repository.save(webBooking);

        Iterable<WebBooking> findAll = repository.findAll();

        assertThat(findAll).hasSize(1);
        webBooking.setId(1L);
        assertThat(findAll).containsOnly(webBooking);
    }
}

Pour suivre cet exemple, vous devez utiliser ces dépendances:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.197</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.9.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
2
Philipp Wirth

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 le package TestEntityManager from org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager afin de valider l'état de l'entité.

1
Przemek Nowak

Dans la dernière version de spring boot 2.1.1.RELEASE, la procédure est simple:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SampleApplication.class)
public class CustomerRepositoryIntegrationTest {

    @Autowired
    CustomerRepository repository;

    @Test
    public void myTest() throws Exception {

        Customer customer = new Customer();
        customer.setId(100l);
        customer.setFirstName("John");
        customer.setLastName("Wick");

        repository.save(customer);

        List<?> queryResult = repository.findByLastName("Wick");

        assertFalse(queryResult.isEmpty());
        assertNotNull(queryResult.get(0));
    }
}

Code complet:

https://github.com/jrichardsz/spring-boot-templates/blob/master/003-hql-database-with-integration-test/src/test/Java/test/CustomerRepositoryIntegrationTest.Java

0
JRichardsz