Интеграционное тестирование Rust с Docker: от теории к практике
Современная разработка на Rust требует не только проверки отдельных функций (юнит-тестов), но и тестирования взаимодействия компонентов с внешними сервисами — базами данных, кэшами, API. Интеграционное тестирование с Docker позволяет изолировать среду, гарантировать воспроизводимость и избежать проблем с "работает на моей машине". В этой статье мы разберем, как настроить интеграционные тесты для Rust-приложений с использованием Docker.
Почему Docker для интеграционного тестирования Rust?
Docker решает три ключевые проблемы:
- Изоляция среды — тесты запускаются в чистом контейнере, без влияния локальных настроек
- Воспроизводимость — одинаковые условия на всех машинах разработчиков и в CI/CD
- Управление зависимостями — автоматический запуск PostgreSQL, Redis, Kafka и других сервисов
Настройка проекта: структура и зависимости
Создадим проект с интеграционными тестами для веб-сервера на Actix-web, который работает с PostgreSQL.
# Cargo.toml
[package]
name = "rust-docker-testing"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
dotenv = "0.15"
[dev-dependencies]
testcontainers = "0.14"
reqwest = { version = "0.11", features = ["json"] }
Обратите внимание на testcontainers — это Rust-библиотека для управления Docker-контейнерами прямо из тестов.
Структура проекта
Рекомендуемая структура для интеграционных тестов:
.
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── models.rs
│ └── handlers.rs
├── tests/
│ ├── common/
│ │ └── mod.rs # Общие утилиты для тестов
│ ├── integration_tests.rs # Основные интеграционные тесты
│ └── docker-compose.yml # Для ручного запуска
├── Dockerfile
└── .env
Шаг 1: Написание интеграционного теста с Docker
Создадим тест, который поднимает PostgreSQL в Docker, запускает миграции и проверяет API.
// tests/integration_tests.rs
use testcontainers::clients;
use testcontainers::images::postgres::Postgres;
use sqlx::postgres::PgPoolOptions;
use std::net::TcpListener;
async fn setup_test_db() -> (String, testcontainers::Container) {
let docker = clients::Cli::default();
let postgres_image = Postgres::default();
let container = docker.run(postgres_image);
let host = container.get_host().unwrap();
let port = container.get_host_port_ipv4(5432).unwrap();
let database_url = format!("postgres://postgres:postgres@{}:{}/postgres", host, port);
// Выполняем миграции
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("Failed to connect to Postgres");
sqlx::query("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)")
.execute(&pool)
.await
.expect("Migration failed");
(database_url, container)
}
#[tokio::test]
async fn test_create_user() {
let (db_url, _container) = setup_test_db().await;
// Запускаем приложение на случайном порту
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = actix_web::HttpServer::new(move || {
actix_web::App::new()
.app_data(actix_web::web::Data::new(db_url.clone()))
.route("/users", actix_web::web::post().to(handlers::create_user))
})
.listen(listener)
.unwrap()
.run();
tokio::spawn(server);
// Ждем запуска сервера
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
// Отправляем запрос
let client = reqwest::Client::new();
let response = client
.post(format!("http://127.0.0.1:{}/users", port))
.json(&serde_json::json!({"name": "Alice"}))
.send()
.await
.unwrap();
assert_eq!(response.status(), 201);
let body: serde_json::Value = response.json().await.unwrap();
assert_eq!(body["name"], "Alice");
}
Шаг 2: Использование Docker Compose для сложных сценариев
Если ваш проект использует несколько сервисов, удобнее применять docker-compose.yml:
// tests/docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Для запуска тестов с Docker Compose используйте следующий подход:
// tests/common/mod.rs
use std::process::Command;
pub fn start_docker_compose() {
Command::new("docker-compose")
.args(["-f", "tests/docker-compose.yml", "up", "-d"])
.status()
.expect("Failed to start Docker Compose");
// Ждем готовности сервисов
std::thread::sleep(std::time::Duration::from_secs(5));
}
pub fn stop_docker_compose() {
Command::new("docker-compose")
.args(["-f", "tests/docker-compose.yml", "down"])
.status()
.expect("Failed to stop Docker Compose");
}
Шаг 3: Интеграция с CI/CD (GitHub Actions)
Чтобы тесты работали в CI, добавьте Docker в ваш workflow:
# .github/workflows/tests.yml
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install Docker
uses: docker/setup-buildx-action@v2
- name: Run integration tests
run: cargo test --test integration_tests -- --ignored
env:
DOCKER_HOST: unix:///var/run/docker.sock