Come testare i repository di Spring Data?


136

Voglio un repository (diciamo, UserRepository) creato con l'aiuto di Spring Data. Sono nuovo di primavera-dati (ma non di primavera) e utilizzo questo tutorial . La mia scelta di tecnologie per gestire il database è JPA 2.1 e Hibernate. Il problema è che non ho idea di come scrivere test unitari per un simile repository.

Prendiamo il create()metodo per esempio. Mentre lavoro per la prima volta, dovrei scrivere un test unitario per questo - ed è qui che mi imbatto in tre problemi:

  • In primo luogo, come posso inserire una simulazione di un EntityManagernell'implementazione inesistente di UserRepositoryun'interfaccia? Spring Data genererebbe un'implementazione basata su questa interfaccia:

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

    Tuttavia, non so come forzarlo a usare un EntityManagermock e altri mock - se avessi scritto l'implementazione da solo, probabilmente avrei un metodo setter per EntityManager, permettendomi di usare il mio mock per il test unitario. (Per quanto riguarda la connettività di database vero e proprio, ho una JpaConfigurationclasse, annotato con @Configuratione @EnableJpaRepositories, a livello di codice che definisce fagioli per DataSource, EntityManagerFactory, EntityManagerecc - ma depositi dovrebbero essere prova-friendly e consentire l'override di queste cose).

  • Secondo, dovrei testare le interazioni? È difficile per me capire quali metodi EntityManagere Querydovrebbero essere chiamati (affine a quello verify(entityManager).createNamedQuery(anyString()).getResultList();), dal momento che non sono io a scrivere l'implementazione.

  • In terzo luogo, dovrei testare in primo luogo i metodi generati dai dati di primavera? Come so, il codice della libreria di terze parti non dovrebbe essere testato a livello di unità, ma solo il codice scritto dagli sviluppatori dovrebbe essere testato a livello di unità. Ma se questo è vero, riporta ancora la prima domanda sulla scena: diciamo, ho un paio di metodi personalizzati per il mio repository, per i quali scriverò l'implementazione, come posso iniettare le mie beffe di EntityManagere Querynel finale, generato repository?

Nota: testerò i miei repository utilizzando sia i test di integrazione che quelli di unità. Per i miei test di integrazione sto usando un database in-memory HSQL e ovviamente non sto usando un database per unit test.

E probabilmente la quarta domanda, è corretto testare la corretta creazione del grafico oggetto e il recupero del grafico oggetto nei test di integrazione (diciamo, ho un grafico oggetto complesso definito con Hibernate)?

Aggiornamento: oggi ho continuato a sperimentare l'iniezione simulata - ho creato una classe interna statica per consentire l'iniezione simulata.

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

}

Tuttavia, l'esecuzione di questo test mi dà il seguente stacktrace:

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

Risposte:


118

tl; dr

Per farla breve - non c'è modo di testare unitariamente i repository JPA Spring Data ragionevolmente per un semplice motivo: è un modo ingombrante per deridere tutte le parti dell'API JPA che invochiamo per avviare i repository. I test unitari non hanno molto senso qui, poiché di solito non si scrive alcun codice di implementazione (vedere il paragrafo seguente sulle implementazioni personalizzate) in modo che il test di integrazione sia l'approccio più ragionevole.

Dettagli

Eseguiamo parecchie convalide e impostazioni iniziali per assicurarci di poter avviare solo un'app che non ha query derivate non valide, ecc.

  • Creiamo e memorizziamo nella cache CriteriaQueryistanze per query derivate per assicurarci che i metodi di query non contengano errori di battitura. Ciò richiede l'utilizzo dell'API Criteria e del meta.model.
  • Verifichiamo le query definite manualmente chiedendo EntityManagerdi creare Queryun'istanza per quelle (che innesca efficacemente la convalida della sintassi delle query).
  • Ispezioniamo i Metamodelmetadati sui tipi di dominio gestiti per preparare nuovi controlli ecc.

Tutte le cose che probabilmente rinvieresti in un repository scritto a mano che potrebbero causare l'interruzione dell'applicazione in fase di esecuzione (a causa di query non valide ecc.).

Se ci pensi, non c'è codice che scrivi per i tuoi repository, quindi non è necessario scrivere alcun test unitario . Semplicemente non è necessario in quanto puoi fare affidamento sulla nostra base di test per rilevare bug di base (se ti capita ancora di incontrarne uno, sentiti libero di alzare un biglietto ). Tuttavia, sono sicuramente necessari test di integrazione per testare due aspetti del tuo livello di persistenza in quanto sono gli aspetti relativi al tuo dominio:

  • mappature di entità
  • semantica della query (la sintassi viene comunque verificata ad ogni tentativo di bootstrap).

Test di integrazione

Questo di solito viene fatto utilizzando un database in memoria e casi di test che eseguono il bootstrap di una Spring di ApplicationContextsolito attraverso il framework di contesto di test (come già si fa), pre-popolano il database (inserendo istanze di oggetti attraverso EntityManagero repository, o tramite un semplice File SQL), quindi eseguire i metodi di query per verificarne l'esito.

Test di implementazioni personalizzate

Le parti di implementazione personalizzate del repository sono scritte in modo tale da non dover conoscere l'APP di Spring Data. Sono semplici fagioli primaverili che vengono EntityManageriniettati. Ovviamente potresti voler provare a deridere le interazioni con esso, ma ad essere sinceri, test unitari dell'APP non è stata un'esperienza troppo piacevole per noi e funziona con un sacco di indirette ( EntityManager-> CriteriaBuilder, CriteriaQueryecc.) che finisci con le beffe che ritornano in giro e così via


5
Hai un link a un piccolo esempio di test di integrazione con un database in memoria (ad es. H2)?
Wim Deblauwe,

7
Gli esempi qui usano HSQLDB. Passare a H2 è fondamentalmente una questione di scambio di dipendenza in pom.xml.
Oliver Drotbohm,

3
Grazie, ma speravo di vedere un esempio che precompila il database e / o controlla davvero il database.
Wim Deblauwe,

1
Il link dietro "scritto in un modo" non funziona più. Forse puoi aggiornarlo?
Wim Deblauwe,

1
Quindi, proponi di utilizzare i test di integrazione anziché i test unitari anche per implementazioni personalizzate? E non scrivere affatto test unitari per loro? Tanto per chiarire. Va bene se si. Capisco la ragione (troppo complessa per deridere tutte le cose). Sono nuovo ai test JPA, quindi voglio solo capirlo.
Ruslan Stelmachenko,

48

Con Spring Boot + Spring Data è diventato abbastanza semplice:

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

    @Autowired
    MyRepository subject;

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

La soluzione di @heez richiama l'intero contesto, ciò fa emergere solo ciò che è necessario per far funzionare JPA + Transaction. Si noti che la soluzione sopra mostrerà un database di test in memoria dato che è possibile trovarlo sul percorso di classe.


7
Questo è un test di integrazione , non un test unitario menzionato da OP
Iwo Kucharski,

16
@IwoKucharski. Hai ragione sulla terminologia. Tuttavia: dato che Spring Data implementa l'interfaccia per te, hai molta difficoltà a utilizzare Spring e a quel punto diventa un test di integrazione. Se facevo una domanda come questa, probabilmente avrei anche chiesto un test unitario senza pensare alla terminologia. Quindi non lo vedevo come il punto principale, o addirittura centrale, della questione.
Markus T,

@RunWith(SpringRuner.class)è ora già incluso in @DataJpaTest.
Maroun,

@IwoKucharski, perché questo è un test di integrazione, non un test unitario?
user1182625

@ user1182625 @RunWith(SpringRunner.classavvia il contesto di primavera, il che significa che verifica l'integrazione tra più unità. Il test unitario sta testando una singola unità -> singola classe. Quindi scrivi MyClass sut = new MyClass();e testa l'oggetto sut (sut = servizio sotto test)
Iwo Kucharski il

21

Questo potrebbe arrivare un po 'troppo tardi, ma ho scritto qualcosa per questo scopo. La mia libreria deriderà i metodi di repository crud di base per te e interpreterà la maggior parte delle funzionalità dei tuoi metodi di query. Dovrai iniettare funzionalità per le tue query native, ma il resto è fatto per te.

Guarda:

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

AGGIORNARE

Questo è ora in Maven centrale e in ottima forma.


16

Se stai usando Spring Boot, puoi semplicemente usare @SpringBootTestper caricare il tuo ApplicationContext(che è ciò di cui il tuo stacktrace ti abbaia). Ciò consente di eseguire l'autowire nei repository di dati di primavera. Assicurati di aggiungere in @RunWith(SpringRunner.class)modo da raccogliere le annotazioni specifiche per la primavera:

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

Puoi leggere di più sui test in avvio di primavera nei loro documenti .


Questo è un buon esempio, ma a mio avviso semplicistico. Ci sono situazioni in cui questo test può persino fallire ??
HopeKing,

Non questo di per sé, ma supponiamo che tu volessi testare Predicates (che era il mio caso d'uso) funziona abbastanza bene.
Heez,

1
per me il repository è sempre nullo. Qualsiasi aiuto?
Atul Chaudhary,

Questa è la migliore risposta. In questo modo testate gli script CrudRepo, Entity e DDL che creano le tabelle dell'entità.
MirandaVeracruzDeLaHoyaCardina

Ho scritto un test esattamente come questo. Funziona perfettamente quando l'implementazione del repository utilizza jdbcTemplate. Tuttavia, quando cambio l'implementazione per i dati di primavera (estendendo l'interfaccia dal repository), il test fallisce e userRepository.findOne restituisce null. Qualche idea su come risolverlo?
Rega,

8

Nell'ultima versione di Spring Boot 2.1.1.RELEASE , è semplice come:

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

Codice completo:

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


3
Questo è un 'esempio' piuttosto incompleto: non può essere costruito, i test di "integrazione" usano la stessa configurazione del codice di produzione. Vale a dire. buono a nulla.
Martin Mucha,

Chiedo scusa. Mi frusterò per questo errore. Per favore, prova ancora!
JRichardsz,

Questo funziona anche con 2.0.0.RELEASESpring Boot.
Nital

Dovresti usare db incorporato per questo test
TuGordoBello

7

Quando vuoi davvero scrivere un i-test per un repository di dati di primavera puoi farlo in questo modo:

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

Per seguire questo esempio devi usare queste dipendenze:

<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>

5

Ho risolto questo usando questo modo -

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

4

Con JUnit5 e @DataJpaTesttest apparirà (codice 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))
    }
}

È possibile utilizzare TestEntityManagerdal org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerpacchetto per convalidare lo stato dell'entità.


È sempre meglio generare ID per il bean di entità.
Arundev,

Per Java la seconda riga è: @ExtendWith (value = SpringExtension.class)
AdilOoze,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.