Рубрики
Без рубрики

Интеграционные тесты БД с загрузкой Spring и тестовыми контейнерами

Узнайте, как создавать мощные интеграционные тесты с помощью контейнеров Spring Boot и Test, которые подключаются к образу базы данных с докеризацией

Автор оригинала: Jędrzej Frankowski.

1. Обзор

Spring Data JPA предоставляет простой способ создания запросов к базе данных и их тестирования с помощью встроенной базы данных H2.

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

В этом уроке мы продемонстрируем как использовать Тестовые контейнеры для интеграционного тестирования с Spring Data JPA и базой данных PostgreSQL.

В нашем предыдущем уроке мы создали несколько запросов к базе данных, используя в основном аннотацию @Query , которую мы сейчас проверим.

2. Конфигурация

Чтобы использовать базу данных PostgreSQL в наших тестах, мы должны добавить зависимость Test containers с test scope и драйвер PostgreSQL в наш pom.xml :


    org.testcontainers
    postgresql
    1.10.6
    test


    org.postgresql
    postgresql
    42.2.5

Давайте также создадим файл application.properties в каталоге ресурсов тестирования, в котором мы проинструктируем Spring использовать соответствующий класс драйверов и создавать и удалять схему при каждом запуске теста:

spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=create-drop

3. Однократное Использование Теста

Чтобы начать использовать экземпляр PostgreSQL в одном тестовом классе, мы должны сначала создать определение контейнера, а затем использовать его параметры для установления соединения:

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(initializers = {UserRepositoryTCIntegrationTest.Initializer.class})
public class UserRepositoryTCIntegrationTest extends UserRepositoryCommonIntegrationTests {

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1")
      .withDatabaseName("integration-tests-db")
      .withUsername("sa")
      .withPassword("sa");

    static class Initializer
      implements ApplicationContextInitializer {
        public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
            TestPropertyValues.of(
              "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(),
              "spring.datasource.username=" + postgreSQLContainer.getUsername(),
              "spring.datasource.password=" + postgreSQLContainer.getPassword()
            ).applyTo(configurableApplicationContext.getEnvironment());
        }
    }
}

В приведенном выше примере мы использовали @ClassRule из JUnit для настройки контейнера базы данных перед выполнением методов тестирования . Мы также создали статический внутренний класс, который реализует ApplicationContextInitializer. В качестве последнего шага мы применили аннотацию @ContextConfiguration к нашему тестовому классу с классом инициализатора в качестве параметра.

Выполнив эти три действия, мы можем задать свойства соединения до публикации контекста Spring.

Теперь давайте используем два запроса на ОБНОВЛЕНИЕ из предыдущей статьи:

@Modifying
@Query("update User u set u.status = :status where u.name = :name")
int updateUserSetStatusForName(@Param("status") Integer status, 
  @Param("name") String name);

@Modifying
@Query(value = "UPDATE Users u SET u.status = ? WHERE u.name = ?", 
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

И протестируйте их в настроенной среде:

@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationJPQL_ThenModifyMatchingUsers(){
    insertUsers();
    int updatedUsersSize = userRepository.updateUserSetStatusForName(0, "SAMPLE");
    assertThat(updatedUsersSize).isEqualTo(2);
}

@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers(){
    insertUsers();
    int updatedUsersSize = userRepository.updateUserSetStatusForNameNative(0, "SAMPLE");
    assertThat(updatedUsersSize).isEqualTo(2);
}

private void insertUsers() {
    userRepository.save(new User("SAMPLE", "[email protected]", 1));
    userRepository.save(new User("SAMPLE1", "[email protected]", 1));
    userRepository.save(new User("SAMPLE", "[email protected]", 1));
    userRepository.save(new User("SAMPLE3", "[email protected]", 1));
    userRepository.flush();
}

В приведенном выше сценарии первый тест заканчивается успешно, но второй выдает InvalidDataAccessResourceUsageException с сообщением:

Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist

Если бы мы выполнили одни и те же тесты с использованием встроенной базы данных H2, оба теста были бы успешно завершены, но PostgreSQL не принимает псевдонимы в предложении SET. Мы можем быстро исправить запрос, удалив проблемный псевдоним:

@Modifying
@Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?", 
  nativeQuery = true)
int updateUserSetStatusForNameNative(Integer status, String name);

На этот раз оба теста завершаются успешно. В этом примере мы использовали тестовые контейнеры для выявления проблемы с собственным запросом, которая в противном случае была бы обнаружена после переключения на реальную производственную базу данных. Мы также должны заметить, что использование запросов JPQL в целом безопаснее, поскольку Spring правильно переводит их в зависимости от используемого поставщика базы данных.

4. Общий экземпляр Базы данных

В предыдущем параграфе мы описали, как использовать тестовые контейнеры в одном тесте. В реальном сценарии мы хотели бы повторно использовать один и тот же контейнер базы данных в нескольких тестах из-за относительно длительного времени запуска.

Теперь давайте создадим общий класс для создания контейнера базы данных, расширив PostgreSQLContainer и переопределив методы start() и stop() :

public class BaeldungPostgresqlContainer extends PostgreSQLContainer {
    private static final String IMAGE_VERSION = "postgres:11.1";
    private static BaeldungPostgresqlContainer container;

    private BaeldungPostgresqlContainer() {
        super(IMAGE_VERSION);
    }

    public static BaeldungPostgresqlContainer getInstance() {
        if (container == null) {
            container = new BaeldungPostgresqlContainer();
        }
        return container;
    }

    @Override
    public void start() {
        super.start();
        System.setProperty("DB_URL", container.getJdbcUrl());
        System.setProperty("DB_USERNAME", container.getUsername());
        System.setProperty("DB_PASSWORD", container.getPassword());
    }

    @Override
    public void stop() {
        //do nothing, JVM handles shut down
    }
}

Оставляя метод stop() пустым, мы позволяем JVM обрабатывать завершение работы контейнера. Мы также реализуем простой одноэлементный шаблон, в котором только первый тест запускает запуск контейнера, а каждый последующий тест использует существующий экземпляр. В методе start() мы используем System#setProperty для установки параметров подключения в качестве переменных среды.

Теперь мы можем поместить их в наш файл application.properties :

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

Теперь давайте используем наш служебный класс в определении теста:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTCAutoIntegrationTest {

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance();

    // tests
}

Как и в предыдущих примерах, мы применили аннотацию @ClassRule к полю, содержащему определение контейнера. Таким образом, свойства Источника данных соединения заполняются правильными значениями перед созданием контекста Spring.

Теперь мы можем реализовать несколько тестов, используя один и тот же экземпляр базы данных , просто определив @ClassRule аннотированное поле, созданное с помощью нашего служебного класса BaeldungPostgresqlContainer|/.

5. Заключение

В этой статье мы проиллюстрировали способы выполнения тестов на реальном экземпляре базы данных с использованием тестовых контейнеров.

Мы рассмотрели примеры использования одного теста с использованием механизма ApplicationContextInitializer из Spring, а также реализации класса для повторного создания экземпляра базы данных.

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

Как всегда, полный код, используемый в этой статье, доступен на GitHub .