Библиотека 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?