Bagaimana cara menguji repositori Data Spring?

136

Saya ingin repositori (katakanlah, UserRepository) dibuat dengan bantuan Spring Data. Saya baru mengenal spring-data (tetapi tidak untuk spring) dan saya menggunakan tutorial ini . Pilihan teknologi saya untuk menangani database adalah JPA 2.1 dan Hibernate. Masalahnya adalah saya tidak tahu bagaimana menulis unit test untuk repositori tersebut.

Mari kita ambil create()metode misalnya. Saat saya mengerjakan tes pertama, saya seharusnya menulis unit test untuk itu - dan di situlah saya menabrak tiga masalah:

  • Pertama, bagaimana cara menyuntikkan tiruan EntityManagerke dalam implementasi UserRepositoryantarmuka yang tidak ada? Spring Data akan menghasilkan implementasi berdasarkan antarmuka ini:

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

    Namun, saya tidak tahu bagaimana memaksanya untuk menggunakan EntityManagertiruan dan tiruan lainnya - jika saya telah menulis implementasinya sendiri, saya mungkin akan memiliki metode setter untuk EntityManager, memungkinkan saya untuk menggunakan tiruan saya untuk pengujian unit. (Adapun konektivitas database sebenarnya, saya memiliki JpaConfigurationkelas, dijelaskan dengan @Configurationdan @EnableJpaRepositories, yang pemrograman mendefinisikan kacang untuk DataSource, EntityManagerFactory, EntityManagerdll - tapi repositori harus ramah-test dan memungkinkan untuk mengesampingkan hal-hal ini).

  • Kedua, haruskah saya menguji interaksi? Sulit bagi saya untuk mencari tahu metode apa EntityManagerdan Queryseharusnya dipanggil (seperti itu verify(entityManager).createNamedQuery(anyString()).getResultList();), karena bukan saya yang menulis implementasinya.

  • Ketiga, apakah saya seharusnya menguji unit metode Spring-Data yang dihasilkan di tempat pertama? Seperti yang saya tahu, kode perpustakaan pihak ketiga tidak seharusnya diuji unit - hanya kode yang ditulis sendiri oleh pengembang yang seharusnya diuji unit. Tetapi jika itu benar, itu masih membawa pertanyaan pertama kembali ke tempat kejadian: katakanlah, saya punya beberapa metode khusus untuk repositori saya, di mana saya akan menulis implementasi, bagaimana saya menyuntikkan ejekan saya EntityManagerdan Queryke final, dihasilkan gudang?

Catatan: Saya akan uji-mengemudi repositori saya menggunakan kedua integrasi dan unit test. Untuk pengujian integrasi, saya menggunakan basis data dalam-memori HSQL, dan saya jelas tidak menggunakan basis data untuk pengujian unit.

Dan mungkin pertanyaan keempat, apakah benar untuk menguji pembuatan grafik objek yang benar dan pengambilan grafik objek dalam tes integrasi (katakanlah, saya memiliki grafik objek kompleks yang didefinisikan dengan Hibernate)?

Pembaruan: hari ini saya terus bereksperimen dengan injeksi tiruan - Saya membuat kelas dalam statis untuk memungkinkan injeksi tiruan.

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

}

Namun, menjalankan tes ini memberi saya stacktrace berikut:

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

Jawaban:

118

tl; dr

Untuk membuatnya singkat - tidak ada cara untuk menguji unit repositori Spring Data JPA secara wajar untuk alasan sederhana: ini adalah cara yang rumit untuk mengejek semua bagian dari API JPA yang kami minta untuk mem-bootstrap repositori. Tes unit tidak masuk akal di sini, karena Anda biasanya tidak menulis kode implementasi sendiri (lihat paragraf di bawah tentang implementasi kustom) sehingga pengujian integrasi adalah pendekatan yang paling masuk akal.

Detail

Kami melakukan cukup banyak validasi dan pengaturan dimuka untuk memastikan Anda hanya bisa mem-bootstrap aplikasi yang tidak memiliki kueri turunan yang tidak valid, dll.

  • Kami membuat dan menyimpan CriteriaQueryinstance untuk kueri turunan untuk memastikan metode kueri tidak mengandung salah ketik. Ini membutuhkan kerja dengan API Kriteria serta meta.model.
  • Kami memverifikasi kueri yang ditentukan secara manual dengan meminta EntityManageruntuk membuat Queryinstance untuk mereka (yang secara efektif memicu validasi sintaksis kueri).
  • Kami memeriksa Metamodelmeta-data tentang jenis domain yang ditangani untuk menyiapkan is-baru cek dll.

Semua hal yang mungkin Anda tunda dalam repositori tulisan tangan yang dapat menyebabkan aplikasi rusak saat runtime (karena kueri yang tidak valid, dll.).

Jika Anda memikirkannya, tidak ada kode yang Anda tulis untuk repositori Anda, jadi tidak perlu menulis tes unit apa pun . Tidak perlu karena Anda dapat mengandalkan basis pengujian kami untuk menangkap bug dasar (jika Anda masih menemukan bug, silakan naikkan tiket ). Namun, pasti diperlukan pengujian integrasi untuk menguji dua aspek dari lapisan kegigihan Anda karena merupakan aspek yang terkait dengan domain Anda:

  • pemetaan entitas
  • semantik permintaan (sintaks diverifikasi pada setiap upaya bootstrap).

Tes integrasi

Ini biasanya dilakukan dengan menggunakan database di dalam memori dan menguji kasus yang bootstrap pegas ApplicationContextbiasanya melalui kerangka konteks pengujian (seperti yang sudah Anda lakukan), pra-mengisi database (dengan memasukkan instance objek melalui EntityManageratau repo, atau melalui dataran File SQL) dan kemudian jalankan metode kueri untuk memverifikasi hasilnya.

Menguji implementasi kustom

Bagian implementasi kustom repositori ditulis sedemikian rupa sehingga mereka tidak perlu tahu tentang Spring Data JPA. Mereka adalah kacang Spring polos yang bisa EntityManagerdisuntikkan. Anda tentu saja ingin mencoba mengejek interaksi dengannya tetapi jujur, unit-test JPA belum menjadi pengalaman yang terlalu menyenangkan bagi kami serta bekerja dengan cukup banyak tipuan ( EntityManager-> CriteriaBuilder, CriteriaQuerydll) jadi Anda berakhir dengan mengolok-olok kembali mengolok-olok dan sebagainya.

Oliver Drotbohm
sumber
5
Apakah Anda memiliki tautan ke contoh kecil uji integrasi dengan basis data dalam memori (mis., H2)?
Wim Deblauwe
7
Contoh di sini menggunakan HSQLDB. Beralih ke H2 pada dasarnya adalah masalah pertukaran ketergantungan dalam pom.xml.
Oliver Drotbohm
3
Terima kasih tetapi saya berharap untuk melihat contoh yang mempopulasi basis data dan / atau benar-benar memeriksa basis data.
Wim Deblauwe
1
Tautan di belakang "ditulis dengan cara" tidak berfungsi lagi. Mungkin Anda bisa memperbaruinya?
Wim Deblauwe
1
Jadi, Anda mengusulkan untuk menggunakan tes integrasi, bukan tes unit untuk implementasi kustom juga? Dan tidak menulis tes unit untuk mereka sama sekali? Hanya untuk mengklarifikasi. Tidak apa-apa jika ya. Saya mengerti alasannya (terlalu rumit untuk mengejek semua hal). Saya baru dalam pengujian JPA jadi saya hanya ingin mengetahuinya.
Ruslan Stelmachenko
48

Dengan Spring Boot + Spring Data menjadi sangat mudah:

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

    @Autowired
    MyRepository subject;

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

Solusi oleh @heez memunculkan konteks penuh, ini hanya memunculkan apa yang diperlukan agar Transaksi JPA + berfungsi. Perhatikan bahwa solusi di atas akan memunculkan database uji memori mengingat seseorang dapat ditemukan di classpath.

Markus T
sumber
7
Ini adalah tes integrasi , bukan tes unit yang disebutkan OP
Iwo Kucharski
16
@IwoKucharski. Anda benar tentang terminologinya. Namun: Mengingat Spring Data mengimplementasikan antarmuka untuk Anda, Anda sulit sekali menggunakan Spring, dan pada saat itu menjadi uji integrasi. Jika saya mengajukan pertanyaan seperti ini, saya mungkin juga meminta unit test tanpa memikirkan terminologinya. Jadi saya tidak melihat itu sebagai pokok, atau bahkan pusat, dari pertanyaan.
Markus T
@RunWith(SpringRuner.class)sekarang sudah termasuk dalam @DataJpaTest.
Maroun
@IwoKucharski, mengapa ini adalah tes integrasi, bukan uji unit?
user1182625
@ user1182625 @RunWith(SpringRunner.classmemulai konteks pegas yang berarti memeriksa integrasi antara beberapa unit. Uji unit menguji satu unit -> kelas tunggal. Kemudian Anda menulis MyClass sut = new MyClass();dan menguji objek sut (sut = layanan yang diuji)
Iwo Kucharski
21

Ini mungkin agak terlambat, tetapi saya telah menulis sesuatu untuk tujuan ini. Perpustakaan saya akan mengejek metode repositori crud dasar untuk Anda serta menafsirkan sebagian besar fungsi metode kueri Anda. Anda harus menyuntikkan fungsionalitas untuk kueri asli Anda sendiri, tetapi sisanya dilakukan untuk Anda.

Lihatlah:

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

MEMPERBARUI

Ini sekarang di pusat Maven dan dalam kondisi yang cukup baik.

Milad Naseri
sumber
16

Jika Anda menggunakan Spring Boot, Anda dapat menggunakan @SpringBootTestuntuk memuat di ApplicationContext(yang merupakan apa yang Anda ketahui tentang stacktrace). Ini memungkinkan Anda untuk autowire di repositori data pegas Anda. Pastikan untuk menambahkan @RunWith(SpringRunner.class)agar anotasi khusus pegas diambil:

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

Anda dapat membaca lebih lanjut tentang pengujian pada booting musim semi di dokumen mereka .

heez
sumber
Ini adalah contoh yang cukup bagus, tetapi menurut saya sederhana. Apakah ada situasi di mana tes ini bahkan bisa gagal ??
HopeKing
Bukan yang ini saja, tapi anggaplah Anda ingin menguji Predicates (yang merupakan kasus penggunaan saya) itu berfungsi dengan baik.
heez
1
bagi saya repositori selalu nol. Ada bantuan?
Atul Chaudhary
Ini adalah jawaban terbaik. Dengan cara ini Anda menguji skrip CrudRepo, Entity dan DDL yang membuat tabel Entity.
MirandaVeracruzDeLaHoyaCardina
Saya telah menulis ujian persis seperti ini. Ini berfungsi dengan baik ketika implementasi dari Repositori menggunakan jdbcTemplate. Namun, ketika saya mengubah implementasi untuk pegas-data (dengan memperluas antarmuka dari Repositori), pengujian gagal dan userRepository.findOne mengembalikan nol. Adakah ide untuk menyelesaikannya?
Rega
8

Dalam versi terakhir dari booting musim semi 2.1.1.RELEASE , sederhana seperti:

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

Kode lengkap:

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

JRichardsz
sumber
3
Ini 'contoh' agak tidak lengkap: tidak dapat dibangun, tes "integrasi" menggunakan konfigurasi yang sama dengan kode produksi. Yaitu. baik untuk apa pun.
Martin Mucha
Saya minta maaf. Saya akan mencambuk saya karena kesalahan ini. Silakan coba sekali lagi!
JRichardsz
Ini juga berfungsi dengan 2.0.0.RELEASESpring Boot.
Nital
Anda harus menggunakan embedded db untuk tes ini
TuGordoBello
7

Ketika Anda benar-benar ingin menulis i-test untuk repositori data pegas Anda dapat melakukannya seperti ini:

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

Untuk mengikuti contoh ini Anda harus menggunakan dependensi ini:

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

Saya memecahkan ini dengan menggunakan cara ini -

    @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
sumber
4

Dengan JUnit5 dan @DataJpaTesttes akan terlihat seperti (kode 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))
    }
}

Anda dapat menggunakan TestEntityManagerdari org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerpaket untuk memvalidasi status entitas.

Przemek Nowak
sumber
Selalu lebih baik untuk menghasilkan Id untuk kacang entitas.
Arundev
Untuk Java, baris kedua adalah: @ExtendWith (value = SpringExtension.class)
AdilOoze