Spring Data veri havuzlarını nasıl test edebilirim?


136

UserRepositorySpring Data yardımıyla oluşturulan bir depo istiyorum . Ben bahar-veri için yeni (ama bahar için değil) ve bu öğretici kullanın . Veritabanıyla başa çıkmak için teknoloji seçimim JPA 2.1 ve Hibernate. Sorun şu ki, böyle bir depo için birim testleri yazma konusunda clueless.

create()Örneğin yöntemi ele alalım. Önce test olarak çalıştığım için, bunun için bir birim testi yazmam gerekiyor - ve bu noktada üç problemle karşılaşıyorum:

  • İlk olarak, EntityManagerbir UserRepositoryarayüzün mevcut olmayan bir uygulamasına nasıl bir sahte enjekte edebilirim ? Spring Data, bu arayüze dayalı bir uygulama oluşturur:

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

    Ancak, bunu bir EntityManagersahte ve diğer alayları kullanmaya nasıl zorlayacağımı bilmiyorum - eğer uygulamayı kendim yazsaydım, muhtemelen alayımı EntityManagerbirim testi için kullanmama izin veren bir ayarlayıcı yöntemim olurdu . (Gerçek veritabanı bağlantısı gelince, ben bir var JpaConfigurationolan açıklamalı sınıfı @Configurationve @EnableJpaRepositoriesprogramlı için fasulye tanımlar DataSource, EntityManagerFactory, EntityManagervb - ama depoları testi dostu olması ve bunları geçersiz kılınmasından izin vermelidir).

  • İkincisi, etkileşimleri test etmeli miyim? Bana ne yöntemlerini anlamaya için çok zor olduğunu EntityManagerve Query(buna benzer çağrılacak gerekiyordu verify(entityManager).createNamedQuery(anyString()).getResultList();) o uygulanmasını yazma kim ben değilim çünkü.

  • Üçüncüsü, ilk etapta Spring-Data tarafından üretilen yöntemleri birim olarak test etmem gerekiyor mu? Bildiğim gibi, üçüncü taraf kütüphane kodunun birim testine tabi tutulmaması gerekiyor - sadece geliştiricilerin kendi yazdığı kodun birim testine tabi tutulması gerekiyor. Ancak bu doğruysa, yine de ilk soruyu sahneye geri getiriyor: diyelim ki, havuzum için birkaç özel yöntemim var, bunun için uygulama yazacağım , oluşturulan mock'larımı EntityManagerve Queryfinaline nasıl enjekte ederim depo?

Not: Depolarımı hem entegrasyon hem de birim testleri kullanarak test sürüşüne tabi tutacağım . Entegrasyon testlerim için bir HSQL bellek içi veritabanı kullanıyorum ve birim testleri için bir veritabanı kullanmıyorum.

Ve muhtemelen dördüncü soru, entegrasyon testlerinde doğru nesne grafiği oluşturma ve nesne grafiği alımını test etmek doğru mu?

Güncelleme: Bugün sahte enjeksiyon ile denemeye devam ettim - Sahte enjeksiyon için statik bir iç sınıf oluşturdum.

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

}

Ancak, bu testi çalıştırmak bana aşağıdaki yığın izini verir:

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

Yanıtlar:


118

tl; Dr.

Kısaca yapmak için - Basit bir nedenden ötürü Spring Data JPA depolarını makul bir şekilde test etmenin bir yolu yoktur: depoları önyüklemek için çağırdığımız JPA API'nin tüm bölümlerini taklit etmek hantal bir yol. Birim testleri burada çok fazla mantıklı değildir, çünkü genellikle kendiniz herhangi bir uygulama kodu yazmazsınız (özel uygulamalarla ilgili aşağıdaki paragrafa bakın), böylece entegrasyon testi en makul yaklaşımdır.

ayrıntılar

Yalnızca geçersiz türetilmiş sorguları vb. Olmayan bir uygulamayı önyükleyebileceğinizden emin olmak için oldukça fazla ön doğrulama ve kurulum yapıyoruz.

  • CriteriaQuerySorgu yöntemlerinin herhangi bir yazım hatası içermediğinden emin olmak için türetilmiş sorgular için örnekler oluşturur ve önbelleğe alırız. Bu, ölçüt API'sı ve meta.model ile çalışmayı gerektirir.
  • Manuel olarak tanımlanan sorguları, bunlar EntityManageriçin bir Queryörnek oluşturmasını (sorgu sözdizimi doğrulamasını etkili bir şekilde tetikler) sorarak doğrularız.
  • Biz incelemek Metamodelhazırlamak için ele alan türleri hakkında için meta-veri olan yeni kontroller vs.

Elle yazılmış bir depoda erteleyebileceğiniz her şey, uygulamanın çalışma zamanında bozulmasına neden olabilir (geçersiz sorgular vb. Nedeniyle).

Bunu düşünürseniz, depolarınız için yazdığınız bir kod yoktur, bu yüzden herhangi bir birim testi yazmanıza gerek yoktur . Temel hataları yakalamak için test tabanımıza güvenebileceğinizden basitçe gerek yoktur (hala bir tanesine rastlarsanız, bilet almaktan çekinmeyin ). Ancak, kalıcılık katmanınızın iki yönünü, alanınızla ilgili yönler olduğu için test etmek için entegrasyon testlerine kesinlikle ihtiyaç vardır:

  • varlık eşlemeleri
  • sorgu anlambilimi (sözdizimi zaten her önyükleme girişiminde doğrulanır).

Entegrasyon testleri

Bu genellikle bir Baharı ApplicationContextgenellikle test bağlamı çerçevesi (zaten yaptığınız gibi) aracılığıyla önyükleyen , veritabanını önceden dolduran ( EntityManagerveya kopyaları aracılığıyla nesne örnekleri ekleyerek veya bir düz yoluyla) bir bellek içi veritabanı ve test senaryoları kullanılarak yapılır. SQL dosyası) ve ardından bunların sonucunu doğrulamak için sorgu yöntemlerini yürütün.

Özel uygulamaları test etme

Deponun özel uygulama bölümleri, Bahar Verileri JPA'sını bilmeleri gerekmeyecek şekilde yazılır . EntityManagerEnjekte edilen sade bahar fasulyeleridir . Elbette onunla etkileşimleri alay etmek ama dürüst olmak gerekirse deneyebilirsin, birim test JPA yanı o indirections oldukça çok çalışır olarak bizim için çok hoş bir deneyim olmamıştır ( EntityManager-> CriteriaBuilder, CriteriaQueryvs.) bu yüzden sonunda alay eden alaylarla sonuçlanır.


5
Bellek içi veritabanıyla (ör. H2) küçük bir bütünleştirme testi örneğine bağlantınız var mı?
Wim Deblauwe

7
Örnekler burada HSQLDB kullanın. H2'ye geçmek temelde pom.xml.
Oliver Drotbohm

3
Teşekkürler, ancak veritabanını önceden dolduran ve / veya gerçekten veritabanını kontrol eden bir örnek görmeyi umuyordum.
Wim Deblauwe

1
"Bir şekilde yazılmış" ifadesinin arkasındaki bağlantı artık çalışmıyor. Belki güncelleyebilirsiniz?
Wim Deblauwe

1
Yani, özel uygulamalar için birim testleri yerine entegrasyon testlerini kullanmayı mı öneriyorsunuz? Ve onlar için birim testleri yazmıyor musunuz? Sadece netleştirmek için. Evet ise sorun değil. Sebebini anlıyorum (her şeyi alay etmek için çok karmaşık). JPA testinde yeniyim, bu yüzden anlamak istiyorum.
Ruslan Stelmachenko

48

Spring Boot + Spring Data ile oldukça kolaylaştı:

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

    @Autowired
    MyRepository subject;

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

@Heez'in çözümü tam bağlamı getirir, bu sadece JPA + Transaction'ın çalışması için gerekenleri ortaya çıkarır. Sınıf çözümünde bulunabileceği göz önüne alındığında, yukarıdaki çözümün bir bellek içi test veritabanı getireceğini unutmayın.


7
Bu OP'nin bahsettiği birim test değil bir entegrasyon testidir
Iwo Kucharski

16
@IwoKucharski. Terminoloji konusunda haklısın. Ancak: Spring Data'nın arayüzü sizin için uyguladığı göz önüne alındığında, Spring'i kullanmakta zorlanırsınız ve bu noktada bir entegrasyon testi haline gelir. Eğer böyle bir soru sorduysam, muhtemelen terminolojiyi düşünmeden bir birim test istedim. Bu yüzden bunu sorunun ana, hatta merkezi noktası olarak görmedim.
Markus T

@RunWith(SpringRuner.class)artık zaten @DataJpaTest.
Maroun

@IwoKucharski, neden birim test değil entegrasyon testi?
user1182625

@ user1182625 @RunWith(SpringRunner.class, bahar içeriğini başlatır, bu da birkaç birim arasındaki entegrasyonu kontrol ettiği anlamına gelir. Birim testi tek bir birimi -> tek sınıfı test ediyor. Sonra MyClass sut = new MyClass();sut nesnesini yazıp test edersiniz (sut = test edilen hizmet)
Iwo Kucharski

21

Bu biraz geç gelebilir, ama bu amaçla bir şeyler yazdım. Kütüphanem, sizin için temel crud veri havuzu yöntemlerini alay edecek ve sorgu yöntemlerinizin işlevlerinin çoğunu yorumlayacaktır. Kendi yerel sorgularınız için işlevsellik enjekte etmeniz gerekir, ancak geri kalanı sizin için yapılır.

Bir göz at:

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

GÜNCELLEME

Bu şimdi Maven'in merkezinde ve oldukça iyi durumda.


16

Spring Boot kullanıyorsanız @SpringBootTest, yüklemek için kullanabilirsiniz ApplicationContext(yığın izlemenizin size havladığı şey budur). Bu, bahar veri havuzlarınızda otomatik bağlantı kurmanızı sağlar. @RunWith(SpringRunner.class)Yaya özel ek açıklamaların toplanacağından emin olun :

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

Dokümanlarında ilkbahar botunda test etme hakkında daha fazla bilgi edinebilirsiniz .


Bu oldukça iyi bir örnek, ancak bence basit. Bu testin başarısız olabileceği durumlar var mı ??
HopeKing

Bu kendi başına değil, ama diyelim ki Predicate(benim kullanım durumum olan) s test etmek istediğinizi gayet iyi çalışıyor.
heez

1
benim için depo her zaman boştur. Herhangi bir yardım?
Atul Chaudhary

Bu en iyi cevap imho. Bu şekilde, Varlık tablolarını oluşturan CrudRepo, Entity ve DDL komut dosyalarını test edersiniz.
MirandaVeracruzDeLaHoyaCardina

Tam olarak böyle bir test yazdım. Deponun uygulanması jdbcTemplate kullandığında mükemmel çalışır. Ancak, bahar verileri için uygulamayı değiştirdiğimde (arabirimi depodan genişleterek), test başarısız olur ve userRepository.findOne null değerini döndürür. Bunu nasıl çözeceğine dair bir fikrin var mı?
Rega

8

Yay çizme son versiyonunda 2.1.1.RELEASE , basit gibidir:

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

Tam kod:

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


3
Bu oldukça 'örnek' tamamlanmamıştır: yapılamaz, "entegrasyon" testleri üretim koduyla aynı yapılandırmayı kullanır. Yani. Hiçbir işe yaramayan.
Martin Mucha

Özür dilerim. Bu hata yüzünden beni kırbaçlayacağım. Lütfen bir kez daha deneyin!
JRichardsz

Bu 2.0.0.RELEASE, Spring Boot ile de çalışır .
Nital

Bu test için gömülü db kullanmalısınız
TuGordoBello

7

Bir yay veri havuzu için gerçekten bir i-testi yazmak istediğinizde bunu şu şekilde yapabilirsiniz:

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

Bu örneği takip etmek için şu bağımlılıkları kullanmanız gerekir:

<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

Bunu şu şekilde kullanarak çözdüm -

    @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

JUnit5 ile @DataJpaTesttest şöyle görünecektir (kotlin kodu):

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

Sen kullanabilirsiniz TestEntityManagergelen org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagervalidate varlık durumuna amacıyla pakete.


Bu varlık fasulye için Id üretmek için her zaman daha iyi bahar.
Arundev

Java için ikinci satır: @ExtendWith (value = SpringExtension.class)
AdilOoze
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.