Pull to refresh
114.44
Haulmont
Создаем современные корпоративные системы

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

Reading time 5 min
Views 22K
Original author: Jędrzej Frankowski

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.

Tags:
Hubs:
+11
Comments 0
Comments Leave a comment

Articles

Information

Website
www.haulmont.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Haulmont