Як протестувати сховища Spring Data?


136

Я хочу сховище (скажімо, UserRepository), створене за допомогою Spring Data. Я новачок у весняних даних (але не у весняних) і використовую цей підручник . Мій вибір технологій для роботи з базою даних - це JPA 2.1 та Hibernate. Проблема полягає в тому, що я не знаю, як писати одиничні тести для такого сховища.

Візьмемо, наприклад, create()метод. Оскільки я працюю на першому тесті, я повинен написати для нього одиничний тест - і саме тут я стикаюся з трьома проблемами:

  • По-перше, як я ввожу макет з EntityManagerв неіснуючу реалізацію UserRepositoryінтерфейсу? Spring Data створює реалізацію на основі цього інтерфейсу:

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

    Однак я не знаю, як змусити його використовувати EntityManagerмакет та інші макети - якби я сам написав реалізацію, я, мабуть, мав би метод встановлення EntityManager, що дозволить мені використовувати моє макет для тестування одиниці. ( Що стосується фактичного підключення до бази даних, у мене є JpaConfigurationклас, з анотацією @Configurationі @EnableJpaRepositories, який програмно визначає боби для DataSource, EntityManagerFactory, і EntityManagerт.д. - але Сховища повинні бути тест-дружній і дозволяють перекриваючи ці речі).

  • По-друге, чи слід перевірити на взаємодію? Мені важко розібратися, які методи EntityManagerі Queryяк слід називати (подібні до цього verify(entityManager).createNamedQuery(anyString()).getResultList();), оскільки це не я, хто пише реалізацію.

  • По-третє, я повинен спочатку перевірити методи, створені Spring-Data? Як я знаю, код сторонньої бібліотеки не повинен бути перевірений одиницею - повинен бути перевірений лише код, який самі розробники пишуть. Але якщо це правда, це все-таки повертає на сцену перше запитання: скажіть, у мене є кілька власних методів для мого сховища, для яких я буду писати реалізацію, як я ввожу свої макети EntityManagerі Queryв остаточний, сформований сховище?

Примітка. Я буду тестово керувати своїми сховищами, використовуючи як інтеграційні, так і одиничні тести. Для моїх інтеграційних тестів я використовую базу даних пам'яті HSQL, і я очевидно не використовую базу даних для одиничних тестів.

І, мабуть, четверте питання, чи правильно перевірити правильність створення об’єктного графіка та пошук об’єктних графіків в тестах інтеграції (скажімо, у мене складний графік об'єктів визначений зі сплячим режимом)?

Оновлення: сьогодні я продовжував експериментувати з макетними ін'єкціями - я створив статичний внутрішній клас, щоб дозволити макетне введення.

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

}

Однак запуск цього тесту дає мені таку стек-трек:

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

Відповіді:


118

тл; д-р

Коротше кажучи - немає способу обгрунтувати тестові сховища Spring Data JPA з простої причини: це спосіб громіздко знущатися над усіма частинами API JPA, які ми закликаємо завантажувати сховища. Тестові одиниці тут не мають великого сенсу, оскільки зазвичай ви самі не пишете жодного коду реалізації (див. Нижче параметр про спеціальні реалізації), так що тестування інтеграції є найбільш розумним підходом.

Деталі

Ми робимо досить багато попередньої перевірки та налаштування, щоб переконатися, що ви можете завантажувати лише додаток, у якому немає недійсних похідних запитів тощо.

  • Ми створюємо та кешуємо CriteriaQueryекземпляри для похідних запитів, щоб переконатися, що методи запиту не містять помилок друку. Для цього потрібна робота з API критеріїв, а також мета.модель.
  • Ми перевіряємо запити, задані вручну, запитуючи EntityManagerстворити Queryекземпляр для тих (що ефективно запускає перевірку синтаксису запитів).
  • Ми перевіряємо Metamodelметадані про типи доменів, які обробляються для підготовки нових перевірок тощо.

Усі речі, які ви, ймовірно, відкладете в рукописний сховище, що може спричинити зрив програми під час виконання (через недійсні запити тощо).

Якщо ви подумаєте про це, у ваших сховищах немає коду, який ви записуєте, тому не потрібно писати жодних тестових одиниць . Просто немає необхідності, оскільки ви можете покластися на нашу тестову базу, щоб ловити основні помилки (якщо ви все-таки натрапили на одного, сміливо піднімайте квиток ). Однак, безумовно, необхідні інтеграційні тести, щоб перевірити два аспекти вашого шару стійкості, оскільки це аспекти, пов’язані з вашим доменом:

  • відображення сутностей
  • семантика запитів (синтаксис перевіряється в кожній спробі завантаження).

Інтеграційні тести

Зазвичай це робиться за допомогою бази даних в пам'яті та тестових випадків, які завантажують Spring ApplicationContextзазвичай через тестовий контекстний фреймворк (як ви вже робите), попередньо заповнюючи базу даних (вставляючи екземпляри об'єктів через EntityManagerабо репо, або через звичайну SQL-файл), а потім виконати методи запиту, щоб перевірити їх результат.

Тестування користувацьких реалізацій

Спеціальні частини репозиторію призначені таким чином, що вони не повинні знати про Spring Data JPA. Вони є звичайними весняними бобами, які отримують EntityManagerін’єкції. Ви, звичайно, хочете спробувати знущатися над взаємодіями з ним, але якщо чесно, тестування одиниці JPA не було для нас надто приємним досвідом, так як це працює з досить великою кількістю непрямих ( EntityManager-> CriteriaBuilder,CriteriaQuery і т.д.) , так що ви закінчуєте з глузливими поверненнями знущань тощо.


5
Чи є у вас посилання на невеликий приклад тесту на інтеграцію з базою даних пам'яті (наприклад, h2)?
Вім Деблауве

7
У прикладах тут використовується HSQLDB. Перехід на H2 - це в основному питання обміну залежністю в pom.xml.
Олівер Дротбом

3
Дякую, але я сподівався побачити приклад, який попередньо заповнює базу даних та / або дійсно перевіряє базу даних.
Вім Деблауве

1
Посилання на "написане способом" більше не працює. Можливо, ви можете оновити його?
Вім Деблауве

1
Отже, ви пропонуєте використовувати тести інтеграції замість одиничних тестів і для користувацьких реалізацій? І взагалі не писати одиничні тести для них? Просто для уточнення. Добре, якщо так. Я розумію причину (занадто складна, щоб знущатися над усіма справами). Я новачок у тестуванні JPA, тому просто хочу це зрозуміти.
Руслан Стельмаченко

48

З Spring Boot + Spring Data це стало досить просто:

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

    @Autowired
    MyRepository subject;

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

Рішення від @heez відображає весь контекст, це лише підводить те, що потрібно для роботи JPA + транзакції. Зауважте, що рішення, наведене вище, створить базу даних тестування пам'яті, враховуючи, що її можна знайти на класі.


7
Це інтеграційний тест, а не одиничний тест, про який згадував ОП
Іво Кучарський

16
@IwoKucharski. Ви маєте рацію щодо термінології. Однак: Враховуючи, що Spring Data реалізує для вас інтерфейс, вам важко використовувати Spring, і в цей момент він стає тестом на інтеграцію. Якщо я задав таке запитання, я, мабуть, також просив провести одиничний тест, не замислюючись про термінологію. Таким чином, я не бачив цього як основного, а то й центрального, питання.
Маркус Т

@RunWith(SpringRuner.class)тепер уже включено до @DataJpaTest.
Марун

@IwoKucharski, чому це тест на інтеграцію, а не тест на одиницю?
користувач1182625

@ user1182625 @RunWith(SpringRunner.classзапускає весняний контекст, що означає перевірку інтеграції між декількома одиницями. Тест одиниці - це тестування однієї одиниці -> одного класу. Тоді ви пишете MyClass sut = new MyClass();і тестуєте sut object (sut = послуга, що перевіряється)
Iwo Kucharski

21

Це може прийти трохи пізно, але я щось саме написав для цієї мети. Моя бібліотека буде висміювати основні методи грубого сховища для вас, а також інтерпретувати більшість функцій ваших методів запитів. Вам доведеться вводити функціональні можливості для власних власних запитів, а решта зроблено за вас.

Поглянь:

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

ОНОВЛЕННЯ

Зараз він знаходиться в центральній частині міста Мейвен і в гарній формі.


16

Якщо ви використовуєте Spring Boot, ви можете просто скористатися @SpringBootTestдля завантаження свого ApplicationContext(це те, про що ви гавкаєте в стек). Це дозволяє автоматично прокладати провід у ваших сховищах весняних даних. Обов’язково додайте, @RunWith(SpringRunner.class)щоб підбирали весняні примітки:

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

Детальніше про тестування весняного завантаження ви можете прочитати в їхніх документах .


Це досить хороший приклад, але спрощений на мій погляд. Чи є ситуації, в яких цей тест може навіть провалитись ??
HopeKing

Не ця сама по собі, але припустимо, ви хотіли перевірити Predicates (що було моїм випадком використання), він працює досить добре.
heez

1
для мене сховище завжди є недійсним. Будь-яка допомога?
Атул Чадхарі

Це найкраща відповідь. Таким чином ви протестуєте сценарії CrudRepo, Entity та DDL, які створюють таблиці (и) сутності.
MirandaVeracruzDeLaHoyaCardina

Я написав тест саме таким. Він прекрасно працює, коли реалізація Репозиторію використовує jdbcTemplate. Однак, коли я змінюю реалізацію для джерел даних (шляхом розширення інтерфейсу з Репозиторію), тест не вдається, а userRepository.findOne повертає null. Будь-які ідеї, як це вирішити?
Рега

8

В останній версії весняного завантаження 2.1.1. РЕЛІЗ , це просто як:

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

Повний код:

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


3
Це доволі неповний "приклад": неможливо побудувати, "інтеграція" тестів використовує таку ж конфігурацію, що і виробничий код. Тобто добре нічого.
Мартін

Я прошу вибачення. Я поб'ю мене через цю помилку. Спробуйте ще раз!
JRichardsz

Це також працює з 2.0.0.RELEASESpring Boot.
Nital

Ви повинні використовувати вбудований db fot для цього тесту
TuGordoBello

7

Коли ви дійсно хочете написати i-тест для весняного сховища даних, ви можете зробити це так:

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

Щоб наслідувати цей приклад, ви повинні використовувати ці залежності:

<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

Я вирішив це таким чином -

    @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 і @DataJpaTestтест буде виглядати так (код Котліна):

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

Ви можете використовувати TestEntityManagerз org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerпакету для перевірки стану сутності.


Його завжди краще весни генерувати Id для сутності.
Арундев,

Для Java другий рядок: @ExtendWith (значення = SpringExtension.class)
AdilOoze
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.