Библиотека 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, то контейнер будет запускаться перед первым тестом и отключаться после последнего. У нас так и будет.
Итак, внутри вышеприведенного тестового класса создадим поле:
@Container
public static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("mydb")
.withUsername("myuser")
.withPassword("mypass")
В вышеприведенном коде:
- Мы задали поднимать контейнер на основе образа postgres:13 — если образ еще не скачан на локальную машину, то при первом запуске теста будет скачан с Docker Hub.
- На основе этого образа будет запущен контейнер с базой данных, причем до первого теста. Для создаваемой базы мы задали имя, логин, пароль (url, по которому можно присоединиться, доступен с помощью метода postgreSQLContainer.getJdbcUrl(), он нам пригодится ниже).
- После завершения последнего теста класса контейнер будет выключен и удален.
Модель данных и репозиторий
У нас в приложении один класс 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 есть статья, и там и здесь используется путь по умолчанию db/changelog/db.changelog-master.yaml до скриптов.
Итоги
Мы рассмотрели библиотеку Testcontainers и использовали ее (совместно с JUnit Jupiter) для поднятия контейнера с базой PostgreSQL 13.
Мы рассмотрели, как заменить при запуске теста application.properties с помощью ApplicationContextInitializer.
Схему и данные в базе мы создали двумя способами — обычным sql-скриптом и с помощью Liquibase.
Код примера с обоими тестами доступен на GitHub.
Спасибо за статью.
Есть возможность не создавать внутренний Initializer класс. Можно использовать @DynamicPropertySource.
https://spring.io/blog/2020/03/27/dynamicpropertysource-in-spring-framework-5-2-5-and-spring-boot-2-2-6
Отличный сайт! Вопрос по статье. Есть ли возможность поднять контейнер описанный в docker-compose?