Comment tester les référentiels Spring Data?

136

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 tutoriel . Mon choix de technologies pour traiter la base de données 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 la create()méthode par exemple. Comme je travaille d'abord sur le test, je suis censé écrire un test unitaire pour cela - et c'est là que je rencontre trois problèmes:

  • Tout d'abord, comment puis-je injecter une maquette d'un EntityManagerdans l'implémentation non existante d'une UserRepositoryinterface? 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 un EntityManagersimulacre 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 base de données réelle, j'ai une JpaConfigurationclasse, annotée avec @Configurationet @EnableJpaRepositoriesqui définit pour les haricots programme DataSource, EntityManagerFactory, EntityManageretc. - mais les dépôts doivent être faciles à tester et permettre la modification de ces choses).

  • Deuxièmement, dois-je tester les interactions? Il m'est difficile de comprendre quelles méthodes de EntityManageret Querysont censées être appelées (comme cela verify(entityManager).createNamedQuery(anyString()).getResultList();), car ce n'est pas moi qui écris l'implémentation.

  • Troisièmement, suis-je censé tester les méthodes générées par Spring-Data en premier lieu? Comme je le sais, le code de la bibliothèque tierce n'est pas censé être testé unitaire - seul le code que les développeurs écrivent eux-mêmes est censé être testé unitaire. Mais si c'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 j'écrirai l'implémentation, comment injecter mes simulations de EntityManageret Querydans le final, généré dépôt?

Remarque: Je vais tester mes référentiels en utilisant à la fois 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 correcte du graphe d'objet et la récupération du graphe d'objet dans les tests d'intégration (par exemple, j'ai un graphe d'objet 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 fictive.

@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, exécuter 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
user1797032
la source

Réponses:

118

tl; dr

Pour faire court, il n'y a aucun moyen de tester les référentiels Spring Data JPA de manière raisonnable pour une raison simple: c'est un moyen fastidieux de se moquer de toutes les parties de l'API JPA que nous appelons pour amorcer les référentiels. De toute façon, les tests unitaires n'ont pas beaucoup de sens ici, car vous n'écrivez généralement pas de code d'implémentation vous-même (voir le paragraphe ci-dessous sur les implémentations personnalisées), de sorte que les tests d'intégration sont l'approche la plus raisonnable.

Détails

Nous faisons beaucoup de validation et de configuration initiales pour nous assurer que vous ne pouvez démarrer qu'une application qui n'a pas de requêtes dérivées invalides, etc.

  • Nous créons et mettons en cache des CriteriaQueryinstances 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 meta.model.
  • Nous vérifions les requêtes définies manuellement en demandant au EntityManagerpour créer une Queryinstance pour celles-ci (ce qui déclenche efficacement la validation de la syntaxe de la requête).
  • Nous inspectons les Metamodelméta-données sur les types de domaines traités pour préparer de nouvelles vérifications, etc.

Tous les éléments que vous reporteriez probablement dans un référentiel écrit à la main, ce qui pourrait provoquer la rupture 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 unitaires . Ce n'est tout simplement pas nécessaire car vous pouvez vous fier à notre base de test pour détecter les bogues de base (si vous en rencontrez toujours un, n'hésitez pas à lever 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
  • la sémantique de la requête (la syntaxe est de toute façon 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 un Spring ApplicationContextgé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 le EntityManagerréférentiel ou, ou via un simple SQL), 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 Spring Data JPA. Ce sont des haricots de printemps simples qui sont EntityManagerinjectés. Vous voudrez peut-être bien sûr essayer de vous moquer des interactions avec lui, mais pour être honnête, le test unitaire du JPA n'a pas été une expérience trop agréable pour nous et il fonctionne avec de nombreuses indirections ( EntityManager-> CriteriaBuilder, CriteriaQueryetc.) donc que vous vous retrouvez avec des moqueries qui reviennent des moqueries, etc.

Oliver Drotbohm
la source
5
Avez-vous un lien vers un petit exemple de test d'intégration avec une base de données en mémoire (par exemple h2)?
Wim Deblauwe
7
Les exemples ici utilisent HSQLDB. Passer à H2 consiste essentiellement à échanger la dépendance dans le pom.xml.
Oliver Drotbohm
3
Merci mais j'espérais voir un exemple qui pré-remplit la base de données et / ou vérifie vraiment la base de données.
Wim Deblauwe
1
Le lien derrière "écrit d'une manière" ne fonctionne plus. Peut-être pouvez-vous le mettre à jour?
Wim Deblauwe
1
Alors, vous proposez d'utiliser des tests d'intégration au lieu de tests unitaires pour les implémentations personnalisées également? Et pas du tout écrire des tests unitaires pour eux? Juste pour clarifier. C'est bon si oui. Je comprends la raison (trop complexe pour se moquer de toutes les choses). Je suis nouveau dans les tests JPA donc je veux juste le comprendre.
Ruslan Stelmachenko
48

Avec Spring Boot + Spring Data, c'est devenu assez simple:

@RunWith(SpringRunner.class)
@DataJpaTest
public class MyRepositoryTest {

    @Autowired
    MyRepository subject;

    @Test
    public void myTest() throws Exception {
        subject.save(new MyEntity());
    }
}

La solution de @heez évoque le contexte complet, cela ne fait apparaître que ce qui est nécessaire pour que JPA + Transaction fonctionne. Notez que la solution ci-dessus affichera une base de données de test en mémoire étant donné que l'on peut en trouver une sur le chemin de classe.

Markus T
la source
7
Ceci est un test d' intégration , pas un test unitaire mentionné par OP
Iwo Kucharski
16
@IwoKucharski. Vous avez raison sur la terminologie. Cependant: étant donné que Spring Data implémente l'interface pour vous, vous avez du mal à utiliser Spring, et à ce stade, cela devient un test d'intégration. Si je posais une question comme celle-ci, je demandais probablement aussi un test unitaire sans penser à la terminologie. Je n'ai donc pas vu cela comme le point principal, voire central, de la question.
Markus T
@RunWith(SpringRuner.class)est maintenant déjà inclus dans le @DataJpaTest.
Maroun
@IwoKucharski, pourquoi est-ce un test d'intégration, pas un test unitaire?
user1182625
@ user1182625 @RunWith(SpringRunner.classdémarre le contexte de ressort, ce qui signifie qu'il vérifie l'intégration entre plusieurs unités. Le test unitaire teste une seule unité -> une seule classe. Ensuite, vous écrivez MyClass sut = new MyClass();et testez l'objet sut (sut = service sous test)
Iwo Kucharski
21

Cela peut arriver un peu trop tard, mais j'ai écrit quelque chose dans ce but précis. Ma bibliothèque se moquera des méthodes de base du référentiel crud pour vous et interprétera 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 reste est fait pour vous.

Regarde:

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

METTRE À JOUR

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

Milad Naseri
la source
16

Si vous utilisez Spring Boot, vous pouvez simplement utiliser @SpringBootTestpour charger dans votre ApplicationContext(c'est ce à quoi votre stacktrace vous aboie). Cela vous permet de se connecter automatiquement à vos référentiels de données de printemps. Assurez-vous d'ajouter @RunWith(SpringRunner.class)afin que les annotations spécifiques au ressort soient prises 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 dans Spring Boot dans leur documentation .

heez
la source
C'est un assez bon exemple, mais simpliste à mon avis. Y a-t-il des situations dans lesquelles ce test peut même échouer?
HopeKing
Pas celui-ci en soi, mais supposons que vous vouliez tester Predicates (ce qui était mon cas d'utilisation), cela fonctionne assez bien.
heez
1
pour moi, le référentiel est toujours nul. De l'aide?
Atul Chaudhary
C'est à mon humble avis la meilleure réponse. De cette façon, vous testez le CrudRepo, l'entité et les scripts DDL qui créent la ou les tables de l'entité.
MirandaVeracruzDeLaHoyaCardina
J'ai écrit un test exactement comme celui-ci. Cela fonctionne parfaitement lorsque l'implémentation du référentiel utilise jdbcTemplate. Cependant, lorsque je change l'implémentation de spring-data (en étendant l'interface à partir du référentiel), le test échoue et userRepository.findOne renvoie null. Des idées pour résoudre ce problème?
Rega
8

Dans la dernière version de spring boot 2.1.1.RELEASE , c'est simple comme:

@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

JRichardsz
la source
3
Cet 'exemple' est plutôt incomplet: ne peut pas être construit, les tests d '"intégration" utilisent la même configuration que le code de production. C'est à dire. bon à rien.
Martin Mucha
Je m'excuse. Je vais me fouetter à cause de cette erreur. Veuillez réessayer!
JRichardsz
Cela fonctionne aussi avec 2.0.0.RELEASEdu printemps Boot.
Nital
Vous devriez utiliser la base de données intégrée pour ce test
TuGordoBello
7

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>
Philipp Wirth
la source
5

J'ai résolu cela en utilisant de 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;
        .......
        .......
}
Ajay Kumar
la source
4

Avec JUnit5 et le @DataJpaTesttest 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 TestEntityManagerfrom org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerpackage afin de valider l'état de l'entité.

Przemek Nowak
la source
Il est toujours préférable de générer un identifiant pour le bean entité.
Arundev
Pour Java, la deuxième ligne est: @ExtendWith (value = SpringExtension.class)
AdilOoze