Тестировать с использованием реальной базы данных жизненно необходимо в сложных приложениях. Представьте, что вы разрабатываете приложение, а на продакшене используете PostgreSQL. Вы пишете тесты, используя H2, и всё отлично работает… до первой встречи с реальным миром, где схему данных ожидает непредвиденный крах. Именно здесь на помощь приходит Testcontainers.
Testcontainers — это библиотека Java, которая позволяет удобно запускать изолированные Docker-контейнеры для использования в тестах. Она дает возможность поднять реальную базу данных (PostgreSQL, MySQL, MongoDB и другие) в контейнере, протестировать приложение и автоматически уничтожить контейнер после завершения теста. Никаких "бонусов" вроде остатков тестового мусора в вашей локальной машине.
Преимущества Testcontainers
- Реальное поведение: использование той же базы данных, что и на продакшене, минимизирует вероятность неожиданностей.
- Одно и то же окружение везде: контейнеры гарантируют одинаковое окружение для тестов на локальной машине, CI/CD и даже вашем холодильнике, если он поддерживает Docker.
- Чистота: после теста контейнер уничтожается, оставляя вашу систему нетронутой.
- Поддержка многих баз данных: PostgreSQL, MySQL, MariaDB, MongoDB, Cassandra и даже Kafka.
Установка Testcontainers
Зависимости Maven/Gradle
Для начала добавим Testcontainers в проект. Используем PostgreSQL в качестве примера базы данных.
Maven:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
Gradle:
testImplementation("org.testcontainers:postgresql:1.19.0")
Простая настройка Testcontainers с PostgreSQL
Начнем с создания базового теста, который использует контейнер PostgreSQL.
Шаг 1: Подготовка тестового контейнера
Создадим класс с тестами, где мы будем использовать контейнер. Testcontainers позволяет настраивать контейнеры для PostgreSQL следующим образом:
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
public class PostgresContainerTest {
@Test
void testPostgresContainer() {
try (PostgreSQLContainer
postgres = new PostgreSQLContainer<>("postgres:15")) {
// Запускаем контейнер
postgres.start();
// Выводим данные контейнера для проверки
System.out.println("Postgres URL: " + postgres.getJdbcUrl());
System.out.println("Username: " + postgres.getUsername());
System.out.println("Password: " + postgres.getPassword());
// Здесь можно подключиться к базе через JDBC и выполнить тестовые действия
}
}
}
Этот тест запускает контейнер с Postgres версии 15. После вызова start() контейнер поднимается, и Testcontainers автоматически находит доступный порт для базы. После завершения блока try контейнер останавливается.
Шаг 2: Интеграция с Spring Boot
Сделаем так, чтобы Spring Boot использовал этот контейнер в тестах. Настройка делается через @DynamicPropertySource.
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {
@Container
private static final PostgreSQLContainer
POSTGRES = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void overrideProperties(org.springframework.test.context.DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
@Test
void contextLoads() {
// Ваш тест здесь
}
}
Что здесь происходит?
- Аннотация
@Testcontainers: указывает, что мы используем Testcontainers. - Аннотация
@Container: автоматически управляет жизненным циклом контейнера (запуск/остановка). - @DynamicPropertySource: переопределяет свойства
spring.datasource, чтобы Spring использовал параметры контейнера.
Написание теста для репозитория
Добавим тест для репозитория, в котором проверим сохранение сущности в базу.
Сущность User:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
// Геттеры и сеттеры
}
Репозиторий:
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
Тест:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.junit.jupiter.Testcontainers;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@Testcontainers
class UserRepositoryTest {
@Container
private static final PostgreSQLContainer
POSTGRES = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void overrideProperties(org.springframework.test.context.DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void testSaveUser() {
User user = new User();
user.setName("John Doe");
User savedUser = userRepository.save(user);
assertThat(savedUser.getId()).isNotNull();
assertThat(savedUser.getName()).isEqualTo("John Doe");
}
}
Этот тест запускает контейнер базы данных, подключает его к Spring Boot и позволяет протестировать сохранение сущности без необходимости вручную поднимать Postgres.
Особенности и ошибки
- Docker должен быть установлен и запущен: если Docker не работает, ваши тесты просто откажутся запускаться.
- Порт 5432 уже занят? Testcontainers автоматически использует произвольный порт для контейнера, поэтому конфликты исключены.
- Долгая инициализация? первый запуск может занять больше времени из-за загрузки образа из Docker Hub.
Практическое применение
- Реальная среда тестирования: вы можете быть уверены, что ваше приложение будет работать так же в продакшене, как и в тестах.
- Автоматизация CI/CD: включив Testcontainers в пайплайны (например, GitHub Actions или GitLab CI), вы сделаете интеграционные тесты более надежными.
- Динамическое тестирование разных конфигураций: просто меняйте параметры контейнера (например, версию Postgres) и проверяйте, что всё работает корректно.
Testcontainers — это мощное средство для интеграционного тестирования с реальными базами данных, которое делает ваш тестовый процесс более надежным и приближенным к реальной эксплуатации. Учитесь использовать его, и не забудьте, что контейнеры после тестов всё же нужно выключать... Хорошо, что Testcontainers делает это автоматически! 😉
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ