Интеграционные тесты баз данных с помощью Spring Boot и Testcontainers

в 12:01, , рубрики: java, spring framework, testcontainers, Блог компании Haulmont, интеграционное тестирование, Тестирование IT-систем

1. Обзор

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

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

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

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

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

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

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.10.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.5</version>
</dependency>

Также создадим в каталоге ресурсов тестирования файл 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<ConfigurableApplicationContext> {
        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.

Теперь используем два запроса UPDATE из предыдущей статьи:

@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@example.com", 1));
    userRepository.save(new User("SAMPLE1", "email2@example.com", 1));
    userRepository.save(new User("SAMPLE", "email3@example.com", 1));
    userRepository.save(new User("SAMPLE3", "email4@example.com", 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);

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

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

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

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

public class BaeldungPostgresqlContainer extends PostgreSQLContainer<BaeldungPostgresqlContainer> {
    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 самостоятельно обработать завершение работы контейнера. Мы также реализуем простой singleton, в котором только первый тест запускает контейнер, а каждый последующий тест использует существующий экземпляр. В методе 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 к полю с определением контейнера. Таким образом, параметры подключения DataSource заполняются правильными значениями до создания контекста Spring.

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

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

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

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

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

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

Автор: a_belyaev

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js