1. Введение
В этом руководстве мы рассмотрим, как использовать testcontainers в приложении Spring boot при создании integration tests.
Testcontainers — лёгкие, одноразовые экземпляры инфраструктурных сервисов, таких как databases, message brokers и т.д., которые могут запускаться в Docker containers. При написании integration tests мы хотели бы, чтобы эти сервисы были доступны, вместо того чтобы мокать их (mocking) или заниматься setup and teardown с помощью пользовательских scripts, подверженных ошибкам.
Давайте посмотрим, как мы можем настроить database container с помощью testcontainers.
2. Предварительные требования
Сначала нам нужно настроить наш проект для работы с testcontainers. Мы можем скачать и установить Docker с официального сайта: https://www.docker.com/get-started
Далее нам нужно добавить зависимости testcontainers в наш файл pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
</dependency>
<dependency
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
Убедимся, что у нас используются последние версии зависимостей testcontainers junit-jupiter и testcontainers-postgresql. Также убедимся, что используем корректную версию для spring-boot-testcontainers, поскольку в старых версиях Spring Boot некоторые классы могут быть недоступны.
3. Использование Testcontainers в тестах Spring Boot
Spring Boot уже некоторое время поддерживает Testcontainers. Однако с выпуском Spring Boot 3.1 интеграция была дополнительно улучшена. Давайте посмотрим, как старый подход соотносится с новым.
В версиях до Spring Boot 3.1 нам приходилось вручную настраивать Spring Boot для подключения к сервисам, запущенным внутри контейнеров. Это часто приводило к добавлению шаблонного кода с использованием аннотации @DynamicPropertySource для установки необходимых свойств:
@SpringBootTest
@Testcontainers
class MyIntegrationTests {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Test
void myTest() {
// ...
}
@DynamicPropertySource
static void postgresProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
Выше мы пометили тест аннотацией @Testcontainer, чтобы сообщить классу теста, что он использует testcontainers. Затем мы определили экземпляр PostgreSQL container с помощью аннотации @Container. Это запустит PostgreSQL container с указанной версией. Обычно мы бы настроили свойства подключения в файле application.properties. Но поскольку мы используем testcontainers, мы ещё не знаем, какими будут свойства подключения. Поэтому мы используем @DynamicPropertySource, чтобы получить свойства подключения из экземпляра контейнера и установить их в контекст приложения Spring Boot.
Как мы видим, этот подход вовлекает несколько составляющих и получается очень многословным даже для простого теста.
Начиная с Spring Boot 3.1, мы можем использовать аннотацию @ServiceConnection, чтобы упростить этот процесс:
@SpringBootTest
@Testcontainers
class MyIntegrationTests {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Test
void myTest() {
// ...
}
}
Здесь мы заменили аннотацию @DynamicPropertySource на @ServiceConnection. Это устраняет необходимость запоминать все properties для любого отдельного сервиса и позволяет Spring Boot автоматически управлять конфигурацией соединения. Конкретно, это работает так: фреймворк обнаруживает тип контейнера и создаёт соответствующий ConnectionDetails bean. Затем Spring Boot использует его для настройки соединения с сервисом, запущенным в Testcontainer.
Мы можем проверить все поддерживаемые контейнеры в официальной документации Spring Boot.
4. Использование Testcontainers во время разработки
Давайте сделаем ещё один шаг и посмотрим, как мы можем использовать Testcontainers во время разработки с помощью новых возможностей в Spring Boot 3.1.
Раньше мы использовали разные profiles, чтобы переключаться между development и test environments. Но теперь есть лучший способ управлять этим и сделать development environment как можно более похожей на production environment.
Вместо создания test profile мы можем создать новый main method для локального запуска нашего приложения с помощью testcontainers:
public class TestMyApplication {
public static void main(String[] args) {
SpringApplication.from(MyApplication::main).run(args);
}
}
Нам нужно создать этот класс внутри test root package, чтобы зависимости testcontainers оставались только в test scope. В противном случае эти зависимости попадут в полные jar files, что увеличит размер приложения, хотя они не используются в production.
Теперь нам нужно настроить test container для PostgreSQL и сообщить об этом новому main method. Сначала создаём файл конфигурации и аннотируем его @TestConfiguration:
@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16");
}
}
Коротко говоря, мы просто создаём bean, который представляет собой container instance of PostgreSQL. Снова мы аннотируем его с помощью @ServiceConnection, чтобы Spring Boot знал, что он должен управлять его lifecycle, а также configure его для использования в нашем application.
Теперь обновим метод test-main:
public class TestMyApplication {
public static void main(String[] args) {
SpringApplication.from(MyApplication::main)
.with(MyContainersConfiguration.class)
.run(args);
}
}
Наконец, мы можем вызвать метод test-main, чтобы запустить приложение с помощью Maven:
./mvnw spring-boot:test-run
Или используя Gradle:
./gradlew bootTestRun
Это запустит приложение, и контейнеры автоматически запустятся и остановятся вместе с приложением. Как мы видим, maven и gradle plugins уже знают о методе test-main и запустят приложение с его помощью.
5. Сохранение данных контейнера между перезапусками
С приведённой выше конфигурацией мы создаём новый экземпляр контейнера каждый раз при запуске приложения. Поэтому мы теряем все их данные. Однако нам может понадобиться сохранять состояние контейнера между перезапусками. Для этого мы можем использовать аннотацию @RestartScope из Spring Boot DevTools на методах bean, создающих контейнер:
@Bean
@ServiceConnection
@RestartScope
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16");
}
Альтернативно, мы можем воспользоваться экспериментальной функцией повторно используемых контейнеров в Testcontainers, вызвав withReuse(true) у контейнера:
new PostgreSQLContainer<>("postgres:16").withReuse(true);
Это позволит сохранять данные между перезапусками. Кроме того, это ускоряет время запуска приложения, так как мы не разворачиваем новый container каждый раз.
6. Заключение
В этой статье мы показали, как использовать Testcontainers в интеграционных тестах Spring Boot. Мы разобрали, как настроить database container с помощью Testcontainers и как применять аннотацию @ServiceConnection для упрощения процесса. Также мы рассмотрели, как использовать Testcontainers в процессе разработки, воспользовавшись новыми возможностями Spring Boot 3.1.


