Wie teste ich Spring Data-Repositorys?

136

Ich möchte ein Repository (z. B. UserRepository), das mit Hilfe von Spring Data erstellt wurde. Ich bin neu in Spring-Daten (aber nicht in Spring) und benutze dieses Tutorial . Meine Auswahl an Technologien für den Umgang mit der Datenbank ist 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 Test-First arbeite, soll ich einen Unit-Test dafür schreiben - und dort stoße ich auf drei Probleme:

  • Wie füge ich zunächst einen Mock of a EntityManagerin die nicht vorhandene Implementierung einer UserRepositorySchnittstelle 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 soll, einen EntityManagerMock und andere Mocks zu verwenden. Wenn ich die Implementierung selbst geschrieben hätte, hätte ich wahrscheinlich eine Setter-Methode EntityManager, mit der ich meinen Mock für den Unit-Test verwenden kann. (Wie für die tatsächliche Datenbankkonnektivität, ich habe eine JpaConfigurationKlasse, mit Anmerkungen versehen mit @Configurationund @EnableJpaRepositories, die programmatisch Bohnen definiert für DataSource, EntityManagerFactory, EntityManageretc. - aber Repositories sollten für das Überschreiben diese Dinge Testfreundlich und lassen sein).

  • Zweitens sollte ich auf Interaktionen testen? Es fällt mir schwer herauszufinden, welche Methoden EntityManagerund Querywie diese genannt werden sollen (ähnlich verify(entityManager).createNamedQuery(anyString()).getResultList();), da ich nicht die Implementierung schreibe.

  • Drittens soll ich die von Spring Data generierten Methoden zuerst einem Unit-Test unterziehen? Wie ich weiß, soll der Bibliothekscode eines Drittanbieters nicht auf Einheiten getestet werden - nur der Code, den die Entwickler selbst schreiben, soll auf Einheiten getestet werden. Aber wenn das stimmt, bringt es immer noch die erste Frage zurück in die Szene: Sagen wir, ich habe ein paar benutzerdefinierte Methoden für mein Repository, für die ich die Implementierung schreiben werde, wie füge ich meine Mocks von EntityManagerund Queryin das generierte Finale ein Repository?

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

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

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

}

Wenn ich diesen Test durchführe, erhalte 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
user1797032
quelle

Antworten:

118

tl; dr

Um es kurz zu machen: Es gibt aus einem einfachen Grund keine Möglichkeit, Spring Data JPA-Repositorys vernünftig zu testen: Es ist viel umständlich, alle Teile der JPA-API zu verspotten, die wir zum Booten der Repositorys aufrufen. Unit-Tests sind hier sowieso nicht allzu sinnvoll, da Sie normalerweise selbst keinen Implementierungscode schreiben (siehe den folgenden Abschnitt zu benutzerdefinierten Implementierungen), sodass Integrationstests der vernünftigste Ansatz sind.

Einzelheiten

Wir führen eine Menge Vorabvalidierungen und -einstellungen durch, um sicherzustellen, dass Sie nur eine App booten können, die keine ungültigen abgeleiteten Abfragen usw. enthält.

  • Wir erstellen und zwischenspeichern CriteriaQueryInstanzen für abgeleitete Abfragen, um sicherzustellen, dass die Abfragemethoden keine Tippfehler enthalten. Dies erfordert die Arbeit mit der Kriterien-API sowie dem meta.model.
  • Wir überprüfen manuell definierte Abfragen, indem wir die bitten EntityManager, eine QueryInstanz für diese zu erstellen (was effektiv die Validierung der Abfragesyntax auslöst).
  • Wir überprüfen die MetamodelMetadaten zu den Domänentypen, die zur Vorbereitung neuer Prüfungen usw. verwendet werden.

Alle Dinge, die Sie wahrscheinlich in einem handgeschriebenen Repository verschieben würden, was dazu führen könnte, dass die Anwendung zur Laufzeit unterbrochen wird (aufgrund ungültiger Abfragen usw.).

Wenn man darüber nachdenkt, gibt es keinen Code , den Sie für Ihre Repositories schreiben, so gibt es keine Notwendigkeit , alle zu schreiben Einheit Tests. Sie müssen sich einfach nicht auf unsere Testbasis verlassen, um grundlegende Fehler zu erkennen (wenn Sie immer noch auf einen stoßen, können Sie gerne ein Ticket erheben ). Es sind jedoch definitiv Integrationstests erforderlich, um zwei Aspekte Ihrer Persistenzschicht zu testen, da diese die Aspekte sind, die sich auf Ihre Domäne beziehen:

  • Entitätszuordnungen
  • Abfragesemantik (die Syntax wird ohnehin bei jedem Bootstrap-Versuch überprüft).

Integrationstests

Dies erfolgt normalerweise mithilfe einer In-Memory-Datenbank und von Testfällen, die einen Spring ApplicationContextnormalerweise über das Testkontext-Framework booten (wie Sie es bereits getan haben), die Datenbank vorab füllen (indem Sie Objektinstanzen über das EntityManageroder repo oder über eine Ebene einfügen) SQL-Datei) und führen Sie dann die Abfragemethoden aus, um das Ergebnis zu überprüfen.

Testen von benutzerdefinierten Implementierungen

Benutzerdefinierte Implementierungsteile des Repositorys sind so geschrieben, dass sie nichts über Spring Data JPA wissen müssen. Es sind einfache Frühlingsbohnen, die EntityManagerinjiziert werden. Sie könnten natürlich Wanna versuchen , die Interaktionen mit ihm zu verspotten , aber um ehrlich zu sein, Komponententests der PPV hat noch keine allzu angenehme Erfahrung für uns auch , wie es mit ziemlich vielen Umwegen funktioniert ( EntityManager-> CriteriaBuilder, CriteriaQueryetc.) so dass Sie am Ende Mocks haben, die Mocks zurückgeben und so weiter.

Oliver Drotbohm
quelle
5
Haben Sie einen Link zu einem kleinen Beispiel eines Integrationstests mit einer In-Memory-Datenbank (z. B. h2)?
Wim Deblauwe
7
Die Beispiele hier verwenden HSQLDB. Das Umschalten auf H2 ist im Grunde eine Frage des Austauschs der Abhängigkeit in der pom.xml.
Oliver Drotbohm
3
Vielen Dank, aber ich hatte gehofft, ein Beispiel zu sehen, das die Datenbank vorab ausfüllt und / oder die Datenbank wirklich überprüft.
Wim Deblauwe
1
Der Link hinter "in gewisser Weise geschrieben" funktioniert nicht mehr. Vielleicht kannst du es aktualisieren?
Wim Deblauwe
1
Sie schlagen also vor, Integrationstests anstelle von Komponententests auch für benutzerdefinierte Implementierungen zu verwenden? Und überhaupt keine Unit-Tests für sie schreiben? Nur um klarzustellen. Es ist in Ordnung, wenn ja. Ich verstehe den Grund (zu komplex, um alle Dinge zu verspotten). Ich bin neu in JPA-Tests, also möchte ich es nur herausfinden.
Ruslan Stelmachenko
48

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 zeigt den vollständigen Kontext an. Dabei wird nur das angezeigt, was für die Funktion von JPA + Transaction erforderlich ist. Beachten Sie, dass die obige Lösung eine In-Memory-Testdatenbank aufruft, sofern diese im Klassenpfad gefunden wird.

Markus T.
quelle
7
Dies ist ein Integrationstest , kein Unit- Test, den OP erwähnt hat
Iwo Kucharski
16
@IwoKucharski. Sie haben Recht mit der Terminologie. Allerdings: Da Spring Data die Schnittstelle für Sie implementiert, ist es schwierig, Spring zu verwenden, und zu diesem Zeitpunkt wird es zu einem Integrationstest. Wenn ich eine solche Frage stellte, bat ich wahrscheinlich auch um einen Komponententest, ohne über die Terminologie nachzudenken. Daher habe ich das nicht als Haupt- oder sogar zentralen Punkt der Frage gesehen.
Markus T
@RunWith(SpringRuner.class)ist jetzt schon in der enthalten @DataJpaTest.
Maroun
@IwoKucharski, warum ist dies ein Integrationstest, kein Unit-Test?
user1182625
@ user1182625 @RunWith(SpringRunner.classstartet den Federkontext, dh es wird die Integration zwischen mehreren Einheiten überprüft. Unit Test testet eine einzelne Einheit -> einzelne Klasse. Dann schreiben MyClass sut = new MyClass();und testen Sie das Sut-Objekt (Sut = Service im Test)
Iwo Kucharski
21

Das mag etwas zu spät kommen, aber ich habe etwas für diesen Zweck geschrieben. Meine Bibliothek wird die grundlegenden Crud-Repository-Methoden für Sie verspotten und die meisten Funktionen Ihrer Abfragemethoden interpretieren. Sie müssen Funktionen für Ihre eigenen nativen Abfragen einfügen, der Rest wird jedoch für Sie erledigt.

Schau mal:

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

AKTUALISIEREN

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

Milad Naseri
quelle
16

Wenn Sie Spring Boot verwenden, können Sie es einfach verwenden @SpringBootTest, um es in Ihr Boot zu laden ApplicationContext(worüber Ihr Stacktrace Sie bellt). Auf diese Weise können Sie Ihre Spring-Data-Repositorys automatisch verdrahten. Stellen Sie sicher, dass Sie hinzufügen, @RunWith(SpringRunner.class)damit die federbezogenen Anmerkungen erfasst 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 im Frühjahrsstart finden Sie in den entsprechenden Dokumenten .

heez
quelle
Dies ist ein ziemlich gutes Beispiel, aber meiner Ansicht nach simpel. Gibt es Situationen, in denen dieser Test sogar fehlschlagen kann?
HopeKing
Nicht dieses an sich, aber nehmen wir an, Sie wollten Predicates testen (was mein Anwendungsfall war), es funktioniert ganz gut.
Heez
1
Für mich ist das Repository immer null. Irgendeine Hilfe?
Atul Chaudhary
Dies ist imho die beste Antwort. Auf diese Weise testen Sie die Skripte CrudRepo, Entity und DDL, mit denen die Tabellen der Entität erstellt werden.
MirandaVeracruzDeLaHoyaCardina
Ich habe genau wie diesen einen Test geschrieben. Es funktioniert perfekt, wenn die Implementierung des Repositorys jdbcTemplate verwendet. Wenn ich jedoch die Implementierung für Spring-Daten ändere (durch Erweitern der Schnittstelle vom Repository), schlägt der Test fehl und userRepository.findOne gibt null zurück. Irgendwelche Ideen, wie man das löst?
Rega
8

In der letzten Version von Spring Boot 2.1.1.RELEASE ist dies so einfach:

@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

JRichardsz
quelle
3
Dies ist ein eher unvollständiges 'Beispiel': Kann nicht erstellt werden, "Integrationstests" verwenden dieselbe Konfiguration wie Produktionscode. Dh. für nichts gut.
Martin Mucha
Ich entschuldige mich. Ich werde mich wegen dieses Fehlers peitschen. Bitte versuchen Sie es noch einmal!
JRichardsz
Dies funktioniert auch mit 2.0.0.RELEASESpring Boot.
Nital
Sie sollten eingebettete
Datenbank für
7

Wenn Sie wirklich einen i-Test für ein Spring-Daten-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>
Philipp Wirth
quelle
5

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;
        .......
        .......
}
Ajay Kumar
quelle
4

Mit JUnit5 und @DataJpaTestTest sieht es so 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 TestEntityManagerfrom org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerpackage verwenden, um den Entitätsstatus zu überprüfen.

Przemek Nowak
quelle
Es ist immer besser, eine ID für die Entity-Bean zu generieren.
Arundev
Für Java lautet die zweite Zeile: @ExtendWith (value = SpringExtension.class)
AdilOoze