Интеграционное тестирование Rust с Docker: продвинутый уровень
Интеграционное тестирование — критически важный этап разработки на Rust, особенно когда ваше приложение взаимодействует с внешними сервисами: базами данных, очередями сообщений, HTTP-API. Использование Docker позволяет создавать изолированные, воспроизводимые окружения для тестов. В этой статье мы рассмотрим продвинутые техники: от базовой настройки до параллельного выполнения тестов с помощью testcontainers и docker-compose.
Почему Docker для интеграционных тестов?
Традиционные моки и заглушки часто не отражают реального поведения зависимостей. Docker решает эту проблему:
- Изоляция: каждый тест запускается в чистом контейнере.
- Воспроизводимость: одинаковое окружение на всех машинах разработчиков и в CI.
- Параллелизм: можно запускать несколько тестовых баз данных одновременно.
1. Базовая настройка: тестирование с PostgreSQL
Начнем с простого: протестируем функцию, работающую с PostgreSQL. Используем крейт testcontainers — Rust-обертку для управления Docker-контейнерами.
Добавьте зависимости в Cargo.toml:
[dev-dependencies]
testcontainers = "0.18"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
Пример теста:
use sqlx::PgPool;
use testcontainers::clients::Cli;
use testcontainers::images::postgres::Postgres;
#[tokio::test]
async fn test_database_connection() {
let docker = Cli::default();
let postgres_instance = docker.run(Postgres::default());
let connection_string = &format!(
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
postgres_instance.get_host_port_ipv4(5432)
);
let pool = PgPool::connect(connection_string).await.unwrap();
// Выполняем SQL-запрос
let row: (i32,) = sqlx::query_as("SELECT 1 + 1")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(row.0, 2);
}
Важно: контейнер автоматически удаляется после завершения теста (благодаря Drop-реализации).
2. Продвинутая конфигурация: собственный образ с миграциями
Часто требуется предварительно выполнить миграции. Создадим кастомный образ на основе Dockerfile:
# Dockerfile.test
FROM postgres:16-alpine
COPY migrations/ /docker-entrypoint-initdb.d/
В Rust-тесте используем Image:
use testcontainers::images::generic::GenericImage;
#[tokio::test]
async fn test_with_migrations() {
let docker = Cli::default();
let image = GenericImage::new("my-app-test:latest")
.with_wait_for(testcontainers::core::WaitFor::message_on_stdout(
"database system is ready to accept connections"
));
let instance = docker.run(image);
// ... используем instance
}
Для сборки образа в CI можно использовать docker build -f Dockerfile.test -t my-app-test:latest .
3. Параллельное выполнение тестов с Docker Compose
Если ваш сервис требует несколько зависимостей (PostgreSQL + Redis), используйте docker-compose. Создайте файл docker-compose.test.yml:
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: testpass
ports:
- "5432"
redis:
image: redis:7-alpine
ports:
- "6379"
В Rust-тесте поднимаем Compose-окружение:
use testcontainers::clients::Cli;
use testcontainers::images::compose::Compose;
#[tokio::test]
async fn test_with_compose() {
let docker = Cli::default();
let compose = Compose::new("docker-compose.test.yml");
let instance = docker.run(compose);
// Получаем порты для подключения
let pg_port = instance.get_host_port_ipv4(5432);
let redis_port = instance.get_host_port_ipv4(6379);
// Тестируем взаимодействие
let pg_url = format!("postgres://postgres:testpass@127.0.0.1:{}/postgres", pg_port);
let redis_url = format!("redis://127.0.0.1:{}/", redis_port);
// ... логика теста
}
Совет: Для ускорения используйте cargo test -- --test-threads=4 — каждый тест получит свой набор портов.
4. Интеграция с CI/CD (GitHub Actions)
Чтобы тесты работали в CI, убедитесь, что Docker доступен. Пример конфигурации:
# .github/workflows/test.yml
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
docker:
image: docker:27-git
options: --privileged
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Run tests
run: cargo test -- --test-threads=2
env:
TESTCONTAINERS_RYUK_DISABLED: true
Флаг TESTCONTAINERS_RYUK_DISABLED=true отключает демон Ryuk (очистка контейнеров), что ускоряет выполнение в CI.
5. Продвинутые трюки
Вот несколько техник для профессионалов:
- Переиспользование контейнеров: используйте
ReusableContainerдля ускорения повторных запусков. - Сетевые задержки: тестируйте таймауты с помощью
toxiproxy— прокси, эмулирующего сетевые проблемы. - Логирование: включите
RUST_LOG=testcontainers=debugдля отладки.
Пример с Toxiproxy:
use testcontainers::images::toxiproxy::Toxiproxy;
#[tokio::test]
async fn test_network_timeout() {
let docker = Cli::default();
let toxiproxy = docker.run(Toxiproxy::new("my-service:8080"));
// Настраиваем задержку в 5 секунд
toxiproxy.add_to