Как протестировать хранилища данных Spring?

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

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

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

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

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

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

  • В-третьих, я должен сначала протестировать методы spring -Data-generate? Как я знаю, код сторонней библиотеки не должен тестироваться на единицу - только код, который разработчики пишут сами, должен быть протестирован на единицу. Но если это правда, оно все еще возвращает первый вопрос на сцену: скажем, у меня есть несколько настраиваемых методов для моего репозитория, для которых я буду писать реализацию, как мне добавить свои издевательства 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

Ответ 1

TL;DR

Чтобы сделать его коротким - нет пути к unit test Spring репозиториям Data JPA разумно по простой причине: это путь к громоздкому издевательствам над всеми частями API JPA, которые мы вызываем для загрузки репозиториев. Модульные тесты не имеют особого смысла в любом случае, поскольку вы обычно не пишете код реализации самостоятельно (см. Следующий абзац о пользовательских реализациях), чтобы интеграционное тестирование было наиболее разумным.

Подробнее

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

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

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

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

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

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

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

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

Пользовательские части реализации репозитория написаны таким образом, что им не нужно знать о Spring Data JPA. Они являются обычными Spring beans, которые вводят an EntityManager. Разумеется, вы можете попытаться насмехаться над этим взаимодействием, но, честно говоря, юнит-тестирование JPA не было для нас слишком приятным опытом, а также работает с довольно многими направлениями (EntityManagerCriteriaBuilder, CriteriaQuery и т.д.), чтобы вы получили издевательства над mocks и т.д.

Ответ 2

С 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. Обратите внимание, что вышеприведенное решение приведет к созданию базы данных теста памяти, учитывая, что ее можно найти в пути к классам.

Ответ 3

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

Посмотрите:

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

UPDATE

Теперь это в центре Maven и в очень хорошей форме.

Ответ 4

Если вы используете 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"));
  }
}

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

Ответ 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;
        .......
        .......
}

Ответ 6

Если вы действительно хотите написать 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>

Ответ 7

С JUnit5 и @DataJpaTest тест будет выглядеть так (код 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))
    }
}

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

Ответ 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