Как протестировать репозитории Spring Data?

136

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

Давайте возьмем create()метод, например. Поскольку я работаю в тестовом режиме, я должен написать для него модульный тест - и вот тут я сталкиваюсь с тремя проблемами:

  • Во-первых, как мне вставить макет EntityManagerв несуществующую реализацию UserRepositoryинтерфейса? Spring Data будет генерировать реализацию на основе этого интерфейса:

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

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

  • Во-вторых, я должен проверить на взаимодействия? Мне трудно понять, какие методы EntityManagerи Queryкак должны вызываться (сродни этому verify(entityManager).createNamedQuery(anyString()).getResultList();), поскольку не я пишу реализацию.

  • В-третьих, должен ли я сначала тестировать методы, сгенерированные Spring-Data? Как я знаю, сторонний библиотечный код не должен подвергаться модульному тестированию - только код, который сами разработчики пишут, должен подвергаться модульному тестированию. Но если это правда, это все же возвращает первый вопрос на сцену: скажем, у меня есть несколько пользовательских методов для моего репозитория, для которых я буду писать реализацию, как мне внедрить мои макеты EntityManagerи Queryв окончательный, сгенерированный хранилище?

Примечание: я буду тест-драйв моих репозиториев, используя оба интеграционные, так и модульные тесты. Для своих интеграционных тестов я использую базу данных HSQL в памяти и, очевидно, не использую базу данных для модульных тестов.

И, возможно, четвертый вопрос: правильно ли тестировать правильное создание графов объектов и извлечение графов объектов в интеграционных тестах (скажем, у меня сложный граф объектов, определенный с помощью Hibernate)?

Обновление: сегодня я продолжил экспериментировать с имитацией инъекций - я создал статический внутренний класс, чтобы обеспечить симуляцию инъекций.

@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
user1797032
источник

Ответы:

118

ТЛ; др

Короче говоря, нет способа провести модульное тестирование репозиториев Spring Data JPA по простой причине: это громоздкий способ издеваться над всеми частями API JPA, которые мы вызываем для начальной загрузки репозиториев. В любом случае, модульные тесты здесь не имеют особого смысла, поскольку вы, как правило, сами не пишете код реализации (см. Приведенный ниже параграф о пользовательских реализациях), поэтому интеграционное тестирование является наиболее разумным подходом.

подробности

Мы проводим предварительную проверку и настройку, чтобы убедиться, что вы можете только запустить приложение, у которого нет недопустимых производных запросов и т. Д.

  • Создаем и кешируем CriteriaQuery экземпляры для производных запросов, чтобы убедиться, что методы запросов не содержат опечаток. Это требует работы с Criteria API, а также с meta.model.
  • Мы проверяем определенные вручную запросы, прося EntityManagerсоздатьQuery экземпляра для них (что эффективно запускает проверку синтаксиса запросов).
  • Мы проверяем Metamodelналичие метаданных о типах доменов, обрабатываемых для подготовки новых проверок и т. Д.

Все вещи, которые вы, вероятно, отложите в рукописном хранилище, что может привести к сбою приложения во время выполнения (из-за неправильных запросов и т. Д.).

Если вы думаете об этом, вы не пишете код для своих репозиториев, поэтому нет необходимости писать какие-либо модульные тесты. В этом просто нет необходимости, поскольку вы можете положиться на нашу тестовую базу для выявления основных ошибок (если вы все-таки столкнулись с ней, не стесняйтесь поднять заявку ). Тем не менее, безусловно, необходимы интеграционные тесты для тестирования двух аспектов вашего уровня персистентности, поскольку они относятся к вашему домену:

  • сопоставления сущностей
  • семантика запроса (синтаксис проверяется при каждой попытке начальной загрузки в любом случае).

Интеграционные тесты

Обычно это делается с помощью базы данных в памяти и тестовых случаев, которые загружают Spring ApplicationContextобычно через контекстную среду тестирования (как вы уже это делаете), предварительно заполняете базу данных (вставляя экземпляры объектов через EntityManagerрепозиторий или repo или через обычный Файл SQL), а затем выполните методы запроса, чтобы проверить их результат.

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

Пользовательские части реализации репозитория написаны так, что им не нужно знать о Spring Data JPA. Это простые бобы Spring, которые получают EntityManagerинъекцию. Конечно, вы можете попытаться высмеять взаимодействие с ним, но, честно говоря, юнит-тестирование JPA не было для нас слишком приятным, так как оно работает с большим количеством косвенных указаний ( EntityManager-> CriteriaBuilderи CriteriaQueryт. Д.), Поэтому что вы в конечном итоге с насмешками, возвращая насмешки и так далее.

Оливер Дротбом
источник
5
У вас есть ссылка на небольшой пример интеграционного теста с базой данных в памяти (например, h2)?
Вим Deblauwe
7
Примеры здесь используют HSQLDB. Переключение на H2 - это в основном вопрос обмена зависимости в pom.xml.
Оливер Дротбом
3
Спасибо, но я надеялся увидеть пример, который предварительно заполняет базу данных и / или действительно проверяет базу данных.
Вим Deblauwe
1
Ссылка "написано в пути" больше не работает. Может быть, вы можете обновить его?
Вим Deblauwe
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 + Transaction. Обратите внимание, что решение, приведенное выше, вызовет тестовую базу данных в памяти, если ее можно найти в пути к классам.

Маркус Т
источник
7
Это интеграционный тест, а не модульный тест, о котором упоминал OP
Иво Кучарски
16
@IwoKucharski. Вы правы насчет терминологии. Однако: учитывая, что Spring Data реализует интерфейс для вас, вам трудно использовать Spring, и в этот момент он становится интеграционным тестом. Если бы я задал вопрос, подобный этому, я, вероятно, также попросил провести модульное тестирование, не думая о терминологии. Таким образом, я не рассматривал это как главный или даже центральный вопрос.
Маркус Т
@RunWith(SpringRuner.class)теперь уже включен в @DataJpaTest.
Марун
@IwoKucharski, почему это интеграционный тест, а не модульный тест?
user1182625
@ user1182625 @RunWith(SpringRunner.classзапускает контекст пружины, что означает, что он проверяет интеграцию между несколькими модулями. Модульное тестирование - это тестирование одного модуля -> одного класса. Затем вы пишете MyClass sut = new MyClass();и тестируете объект sut (sut = тестируемый сервис)
Иво Кучарски,
21

Это может быть слишком поздно, но я написал кое-что для этой цели. Моя библиотека будет макетировать основные методы хранилища crud для вас, а также интерпретировать большинство функций ваших методов запросов. Вам придется вводить функциональные возможности для собственных запросов, но все остальное сделано за вас.

Взглянуть:

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

ОБНОВИТЬ

Это сейчас в центре Maven и в довольно хорошей форме.

Милад Насери
источник
16

Если вы используете Spring Boot, вы можете просто использовать его @SpringBootTestдля загрузки ApplicationContext(это то, на что лает стэк-трассировка). Это позволяет вам автоматически подключаться к хранилищам данных Spring. Обязательно добавьте, @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"));
  }
}

Вы можете прочитать больше о тестировании в весенней загрузке в их документах .

heez
источник
Это довольно хороший пример, но, на мой взгляд, упрощенный. Есть ли ситуации, в которых этот тест может даже провалиться?
HopeKing
Не этот как таковой, но предположим, что вы хотели протестировать Predicates (который был моим вариантом использования), он работает довольно хорошо.
Боже,
1
для меня хранилище всегда равно нулю. Любая помощь?
Атул Чаудхари
Это imho лучший ответ. Таким образом, вы тестируете сценарии CrudRepo, Entity и DDL, которые создают таблицы (ы) Entity.
МирандаВеракрусDeLaHoyaCardina
Я написал тест точно так же, как этот. Он отлично работает, когда реализация репозитория использует jdbcTemplate. Однако, когда я изменяю реализацию для данных Spring (расширяя интерфейс из репозитория), тест завершается неудачно, и 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

JRichardsz
источник
3
Это довольно неполный «пример»: не может быть построен, «интеграционные» тесты используют ту же конфигурацию, что и рабочий код. То есть. никуда не годится.
Мартин Муха
Я извиняюсь. Я высечу меня из-за этой ошибки. Пожалуйста, попробуйте еще раз!
JRichardsz
Это также работает с 2.0.0.RELEASESpring Boot.
Nital
Вы должны использовать встроенный дБ для этого теста
TuGordoBello
7

Если вы действительно хотите написать i-test для репозитория данных Spring, вы можете сделать это следующим образом:

@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 и @DataJpaTesttest будет выглядеть (код котлина):

@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 (value = SpringExtension.class)
AdilOoze