web-dev-qa-db-ger.com

Wie teste ich Spring Data Repositories?

Ich möchte, dass ein Repository (sprich UserRepository) mit Hilfe von Spring Data erstellt wird. Ich bin neu bei Spring-Data (aber nicht bei Spring) und verwende dieses Tutorial . Ich wähle für den Umgang mit der Datenbank zwischen JPA 2.1 und Hibernate. Das Problem ist, dass ich keine Ahnung habe, wie man Unit-Tests für ein solches Repository schreibt.

Nehmen wir zum Beispiel die create()-Methode. Während ich testweise arbeite, soll ich einen Unit-Test dafür schreiben - und dabei stoße ich auf drei Probleme:

  • Erstens, wie füge ich ein Modell einer EntityManager in die nicht vorhandene Implementierung einer UserRepository-Schnittstelle ein? Spring Data würde eine Implementierung basierend auf dieser Schnittstelle generieren:

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

    Ich weiß jedoch nicht, wie ich es zwingen kann, einen Mock für EntityManager und andere Mocks zu verwenden. Wenn ich die Implementierung selbst geschrieben hätte, hätte ich wahrscheinlich eine Setter-Methode für EntityManager, mit der ich meinen Mock für den Komponententest verwenden kann. (Was die tatsächliche Datenbankkonnektivität angeht, habe ich eine JpaConfiguration-Klasse, die mit @Configuration und @EnableJpaRepositories kommentiert ist, die Beans für DataSource, EntityManagerFactory, EntityManager usw. programmgesteuert definiert.

  • Zweitens, sollte ich auf Interaktionen testen? Es ist schwer für mich herauszufinden, welche Methoden von EntityManager und Query aufgerufen werden sollen (ähnlich wie diese verify(entityManager).createNamedQuery(anyString()).getResultList();), da ich nicht die Person bin, die die Implementierung schreibt.

  • Drittens: Soll ich die von Spring-Data generierten Methoden überhaupt einem Unit-Test unterziehen? Wie ich weiß, soll der Bibliothekscode eines Drittanbieters nicht Unit-Test sein - nur der Code, den die Entwickler selbst schreiben, sollte Unit-Test sein. Aber wenn das stimmt, bringt es immer noch die erste Frage zurück zur Szene: Ich habe ein paar benutzerdefinierte Methoden für mein Repository, für die ich die Implementierung schreiben werde. Wie füge ich meine Vorstellungen von EntityManager und Query in das Finale ein , generiertes Repository?

Hinweis: Ich werde meine Repositorys mit sowohl als auch dem Integrationstest und den Unit-Tests testen. Für meine Integrationstests verwende ich eine HSQL-In-Memory-Datenbank, und offensichtlich verwende ich keine Datenbank für Komponententests.

Und wahrscheinlich die vierte Frage: Ist es korrekt, die korrekte Erstellung von Objektgraphen und das Abrufen von Objektgraphen in den Integrationstests zu testen (z. B. habe ich einen komplexen Objektgraph, der mit Hibernate definiert wurde)?

Update: Heute habe ich weiter mit der Scheininjektion experimentiert - ich habe eine statische innere Klasse erstellt, um die Scheininjektion zu ermöglichen. 

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

}

Beim Ausführen dieses Tests bekomme ich jedoch die folgende Stapelverfolgung:

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

Um es kurz zu machen: Es gibt keine Möglichkeit, Spring Data JPA-Repositorys aus einem einfachen Grund zu testen: Es ist mühsam, alle Teile der JPA-API zu simulieren, die wir zum Bootstrappen der Repositorys verwenden. Komponententests sind hier ohnehin nicht sehr sinnvoll, da Sie normalerweise selbst keinen Implementierungscode schreiben (siehe den folgenden Abschnitt über benutzerdefinierte Implementierungen), so dass Integrationstests der sinnvollste Ansatz sind.

Einzelheiten

Wir machen eine ganze Reihe von Vorabprüfungen und Setups, um sicherzustellen, dass Sie nur eine App bootstrap können, die keine ungültigen abgeleiteten Abfragen usw. enthält.

  • Wir erstellen CriteriaQuery-Instanzen für abgeleitete Abfragen, um sicherzustellen, dass die Abfragemethoden keine Tippfehler enthalten. Dies erfordert das Arbeiten mit der Criteria API sowie dem meta.model.
  • Wir überprüfen manuell definierte Abfragen, indem wir die EntityManager bitten, eine Query-Instanz für diese zu erstellen (die die Abfrage-Syntaxvalidierung effektiv auslöst).
  • Wir prüfen die Metamodel auf Metadaten über die behandelten Domänentypen, um neue Prüfungen usw. vorzubereiten.

Alles, was Sie wahrscheinlich in einem handgeschriebenen Repository verschoben haben, wodurch die Anwendung zur Laufzeit (aufgrund ungültiger Abfragen usw.) möglicherweise unterbrochen wird.

Wenn Sie darüber nachdenken, gibt es keinen Code, den Sie für Ihre Repositorys schreiben. Daher müssen Sie keine unit -Tests schreiben. Es ist einfach nicht nötig, da Sie sich auf unsere Testbasis verlassen können, um grundlegende Fehler zu finden (falls Sie dennoch auf eines stoßen, zögern Sie nicht, ein ticket zu erhöhen). Es gibt jedoch unbedingt Integrationstests, um zwei Aspekte Ihrer Persistenzschicht zu testen, da diese Aspekte Ihre Domäne betreffen: 

  • entitätszuordnungen 
  • abfragesemantik (die Syntax wird trotzdem bei jedem Bootstrap-Versuch überprüft).

Integrationstests

Dies geschieht in der Regel durch die Verwendung einer In-Memory-Datenbank und Testfällen, die eine Spring ApplicationContext bootstrapieren, in der Regel (wie Sie es bereits getan haben) durch das Testkontext-Framework. Führen Sie die Datenbank vorab aus (durch Einfügen von Objektinstanzen über EntityManager oder repo oder via eine einfache SQL-Datei) und führen Sie dann die Abfragemethoden aus, um das Ergebnis zu überprüfen.

Benutzerdefinierte Implementierungen testen

Benutzerdefinierte Implementierungsteile des Repositorys sind in einer Weise geschrieben , die sie nicht über Spring Data JPA wissen müssen. Sie sind einfache Spring-Beans, die eine EntityManager-Injektion erzeugen. Sie möchten natürlich versuchen, die Interaktionen damit zu verspotten, aber um ehrlich zu sein, war es für uns keine allzu angenehme Erfahrung, die JPA zu testen, und es funktioniert mit einer ganzen Reihe von Hinweisen (EntityManager -> CriteriaBuilder, CriteriaQuery usw.) .) so dass Sie mit Mocks enden, die Mocks zurückbringen und so weiter.

88
Oliver Drotbohm

Mit Spring Boot + Spring Data ist es ganz einfach geworden:

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

    @Autowired
    MyRepository subject;

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

Die Lösung von @heez bringt den vollständigen Kontext zum Vorschein, nur das, was für die Arbeit von JPA + Transaction erforderlich ist ..__ Beachten Sie, dass die oben genannte Lösung eine Test-Datenbank im Arbeitsspeicher aufruft, die sich im Klassenpfad befindet .

27
Markus T

Dies mag etwas zu spät kommen, aber ich habe etwas zu diesem Zweck geschrieben. Meine Bibliothek wird die grundlegenden Methoden des Crud-Repositorys simulieren und die meisten Funktionen Ihrer Abfragemethoden interpretieren .. Sie müssen Funktionalitäten für Ihre eigenen systemeigenen Abfragen einfügen, der Rest ist jedoch für Sie erledigt.

Schau mal:

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

UPDATE

Dies ist jetzt in Maven zentral und in ziemlich gutem Zustand.

19
Milad Naseri

Wenn Sie Spring Boot verwenden, können Sie einfach @SpringBootTest verwenden, um Ihre ApplicationContext zu laden (dies ist, worum Sie Ihr Stacktrace bellt). Auf diese Weise können Sie Ihre Quelldaten-Repositories automatisch verwalten. Stellen Sie sicher, dass Sie @RunWith(SpringRunner.class) hinzufügen, damit die frühlingsspezifischen Anmerkungen aufgenommen werden:

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

Weitere Informationen zum Testen von Spring Boot finden Sie in deren docs .

12
heez

Ich habe das auf diese Weise gelöst - 

    @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

Wenn Sie wirklich einen i-Test für ein Frühjahrsdaten-Repository schreiben möchten, können Sie dies folgendermaßen tun:

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

Um diesem Beispiel zu folgen, müssen Sie diese Abhängigkeiten verwenden:

<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

Mit JUnit5 und @DataJpaTest test sieht das aus (kotlin code):

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

Sie können TestEntityManager aus dem org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager-Paket verwenden, um den Entitätsstatus zu überprüfen.

1
Przemek Nowak

In der letzten Version von Spring Boot 2.1.1.RELEASE ist es einfach wie folgt:

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

Vollständiger Code:

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

0
JRichardsz