Тестирование с помощью Testcontainers: как поднять в контейнере тестовую базу

Библиотека Testcontainers позволяет поднять тестовую среду в Docker-контейнере на время тестов, что довольно удобно.

Мы будем тестировать Spring Boot приложение, разработанное на базе PostgreSQL, и поэтому будем поднимать контейнер с образом PostgreSQL (13 версии). Для работы с таким контейнером в библиотеке Testcontainers есть класс PostgreSQLContainer (подкласс GenericContainer). Для многих баз данных есть свои классы-контейнеры.

Подготовка: установим Docker

Поскольку библиотека Testcontainers под капотом поднимает Docker-контейнер, на машине, где происходит тестирование, должен быть установлен Docker. Например, для Windows 10 его можно скачать отсюда.

Maven-зависимости

Чтобы работать с библиотекой Testcontainers, в POM-файл Spring Boot приложения необходимо добавить зависимость:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

Поскольку мы будем работать с PostgreSQL, также добавим зависимости:

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

<dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
   <scope>runtime</scope>
</dependency>

@Testcontainers для JUnit Jupiter

Мы также будем использовать JUnit Jupiter (а не JUnit4), поэтому тестовый класс необходимо аннотировать @Testcontainers:

@SpringBootTest
@Testcontainers
public class IntegrationSqlScriptTest {
   //тесты
}

Следующая аннотация – @Container – тоже подразумевает использование JUnit Jupiter (для JUnit4 вместо @Container нужно использовать @Rule и @ClassRule).

@Container

Внутри класса теста необходимо создать поле, аннотированное с помощью @Container – собственно контейнер, который будет запускаться до и тушиться после тестов. Причем если поле static, то контейнер будет запускаться перед первым тестом и отключаться после последнего. У нас так и будет.

Если же необходимо делать запуск для каждого теста отдельно, просто не ставьте static (но это более ресурсоемко).

Итак, внутри вышеприведенного тестового класса создадим поле:

@Container
public static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:13")
        .withDatabaseName("mydb")
        .withUsername("myuser")
        .withPassword("mypass")

В вышеприведенном коде:

  1. Мы задали поднимать контейнер на основе образа postgres:13 – если образ еще не скачан на локальную машину, то при первом запуске теста будет скачан с Docker Hub.
  2. На основе этого образа будет запущен контейнер с базой данных, причем до первого теста. Для создаваемой базы мы задали имя, логин, пароль (url, по которому можно присоединиться, доступен с помощью метода  postgreSQLContainer.getJdbcUrl(), он нам пригодится ниже).
  3. После завершения последнего теста класса контейнер будет выключен и удален.

Модель данных и репозиторий

У нас в приложении один класс Animal:

@Entity
public class Animal {
    @Id
    @GeneratedValue(strategy= GenerationType.SEQUENCE)
    private long id;
    private String name;
...
}

И соответствующий репозиторий:

public interface AnimalRepository extends JpaRepository<Animal, Long> {
}

Для простоты его методы и будем тестировать (хотя это и бессмысленно).

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

spring.jpa.hibernate.ddl-auto=validate

Создание схемы и заполнение базы

Контейнер поднимается, база в нем присутствует.  Осталась одна проблема – как все же создать схему базы и заполнить базу данными. Для этого мы будем запускать скрипты (два варианта: SQL и Liquibase).

Чтобы использовать Datasource вновь создаваемой базы данных из запускаемого контейнера, нам надо установить свойства этого Datasource в application.properties.

То есть свойства  application.properties:

spring.datasource.url=
spring.datasource.username=
spring.datasource.password=

на время тестов нужно динамически заменить свойствами базы из контейнера. И это можно сделать.

Замена application.properties в ApplicationContextInitializer

Для этого создадим класс-инициализатор (он будет выполнять замену) и укажем его в аннотации @ContextConfiguration тестового класса.

@ContextConfiguration(initializers = {IntegrationSqlScriptTest.Initializer.class})

Итак, класс Initializer (он будет статический прямо внутри тестового класса, хотя это необязательно):

@SpringBootTest
@ContextConfiguration(initializers = {IntegrationSqlScriptTest.Initializer.class})
@Testcontainers
public class IntegrationSqlScriptTest {

    @Autowired
    AnimalRepository animalRepository;

    @Container
    public static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("mydb")
            .withUsername("myuser")
            .withPassword("mypass")
            .withInitScript("db.sql");

    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());
        }
    }
    @Test
    @Transactional
    public void animalsCountShouldBeCorrect() {
        long count = animalRepository.count();
        assertEquals(3, count);

    }

    @Test
    @Transactional
    public void animalsShouldBeCorrect() {
        List<Animal> animals = animalRepository.findAll();
        String[] actualAnimals = {"animal1", "animal2", "animal3"};
        assertArrayEquals(actualAnimals, animals.stream().map(animal -> animal.getName()).toArray());
    }
}

Выше приведен весь код: добавлен AnimalRepository, и два очень простых теста.

При запуске тестов application.properties заменяются свойствами базы из контейнера.

Создание схемы и заполнение базы обычным скриптом

И обратите внимание, что для контейнера PostgreSQLContainer в коде выше задан скрипт с помощью метода

.withInitScript("db.sql");

Скрипт db.sql лежит в папке /resources. В нем создается таблица, таблица заполняется данными и создается последовательность. (Последовательность hibernate_sequence нужна, поскольку стратегия генерации идентификатора в сущности Animal выбрана @GeneratedValue(strategy= GenerationType.SEQUENCE)).

Содержимое db.sql:

CREATE SEQUENCE hibernate_sequence START 100;
create table animal
(
    id       bigint primary key,
    name     varchar(255)
);

insert into animal (id, name)
values (1,'animal1'),
       (2,'animal2'),
       (3,'animal3');

Теперь рассмотрим другой более сложный способ создания схемы и данных в базе.

Создание схемы и заполнение базы с помощью Liquibase

Схему и данные можно создать и с помощью Liquibase-скриптов. В этом случае скрипт db.sql больше не нужен, и строку  .withInitScript(“db.sql”); следует убрать.

Вместо этого нам надо заменить еще одно свойство –  включить spring.liquibase.enabled=true:

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(),
                "spring.liquibase.enabled=true"
        ).applyTo(configurableApplicationContext.getEnvironment());
    }
}

По умолчанию оно задано равным false, чтобы Liquibase-скрипты не запускались при выполнении тестов предыдущего тестового класса с db.sql.

В остальном новый тестовый класс остается таким же, как предыдущий.

Конечно, Liquibase скрипты должны лежать в ресурсах:

Расположение Liquibase-скриптов
Расположение Liquibase-скриптов

Подробнее о Liquibase есть статья, и там и здесь используется путь по умолчанию db/changelog/db.changelog-master.yaml до скриптов.

Итоги

Мы рассмотрели библиотеку Testcontainers и использовали ее (совместно с JUnit Jupiter) для поднятия контейнера с базой PostgreSQL 13.

Мы рассмотрели, как заменить при запуске теста application.properties с помощью ApplicationContextInitializer.

Схему и данные в базе мы создали двумя способами – обычным sql-скриптом и с помощью Liquibase.

Код примера с обоими тестами доступен на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *