Интеграционное тестирование Rust с Docker: от А до Я
Интеграционное тестирование — критически важный этап разработки надёжных приложений на Rust. В отличие от модульных тестов, интеграционные проверяют взаимодействие компонентов с внешними зависимостями: базами данных, кэшами, очередями сообщений. Docker идеально подходит для создания изолированных, воспроизводимых тестовых сред. В этой статье мы разберём, как настроить интеграционное тестирование Rust-проекта с использованием Docker, начиная с базовой архитектуры и заканчивая CI/CD.
1. Почему Docker — лучший выбор для интеграционных тестов Rust
Rust компилируется в нативный код, но его приложения часто зависят от внешних сервисов. Docker решает три ключевые проблемы:
- Изоляция: каждый тест запускается в чистом контейнере, исключая влияние предыдущих запусков.
- Воспроизводимость: точные версии PostgreSQL, Redis или Kafka гарантируют одинаковое поведение на локальной машине и в CI.
- Скорость: Docker-образы кешируются, а Rust-тесты компилируются один раз.
Рассмотрим типовой проект: веб-сервер на Actix-web, работающий с PostgreSQL. Наша цель — написать тест, который поднимает контейнер с БД, выполняет миграции и проверяет CRUD-операции.
2. Настройка тестовой инфраструктуры с Docker Compose
Для удобства используем docker-compose.yml, который описывает все зависимости. Создадим файл в корне проекта:
version: '3.8'
services:
postgres_test:
image: postgres:15-alpine
environment:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
POSTGRES_DB: test_db
ports:
- "5433:5432"
tmpfs: /var/lib/postgresql/data # данные в памяти для скорости
Обратите внимание на tmpfs — это ускоряет тесты, так как данные не пишутся на диск. Порт 5433 выбран, чтобы не конфликтовать с локальной БД.
Теперь напишем Rust-функцию для управления контейнерами. Используем крейт testcontainers — он позволяет программно запускать Docker-контейнеры:
// tests/common/mod.rs
use testcontainers::{clients, images::postgres::Postgres, Container};
pub struct TestDb {
pub container: Container<'static, Postgres>,
pub connection_string: String,
}
impl TestDb {
pub fn new() -> Self {
let docker = clients::Cli::default();
let postgres_image = Postgres::default();
let container = docker.run(postgres_image);
let host = container.get_host();
let port = container.get_host_port_ipv4(5432);
let connection_string = format!(
"postgres://postgres:postgres@{}:{}/postgres",
host, port
);
TestDb { container, connection_string }
}
}
Этот модуль можно переиспользовать во всех интеграционных тестах. Контейнер автоматически удалится при падении тестов или завершении процесса.
3. Написание интеграционных тестов с реальными запросами
Создадим тест, который проверяет вставку и чтение пользователя. Предположим, у нас есть структура User и функции create_user и get_user в основном коде.
// tests/integration_test.rs
mod common;
use sqlx::PgPool;
use your_crate::{create_user, get_user, User};
#[tokio::test]
async fn test_create_and_get_user() {
let test_db = common::TestDb::new();
let pool = PgPool::connect(&test_db.connection_string)
.await
.expect("Failed to connect to DB");
// Выполняем миграции (если используете sqlx)
sqlx::migrate!().run(&pool).await.unwrap();
// Создаём пользователя
let user = User {
name: "Alice".into(),
email: "alice@example.com".into(),
};
let created = create_user(&pool, &user).await.unwrap();
assert_eq!(created.name, "Alice");
// Читаем пользователя
let fetched = get_user(&pool, created.id).await.unwrap();
assert_eq!(fetched.email, "alice@example.com");
}
Важные моменты:
- Тесты асинхронные — используем
#[tokio::test]. - Каждый тест создаёт свой контейнер, обеспечивая полную изоляцию.
- Миграции выполняются внутри теста — это гарантирует актуальную схему.
Для ускорения можно использовать once_cell и запускать контейнер один раз на все тесты, но тогда теряется изоляция. Компромисс — запускать контейнер для каждого файла с тестами.
4. CI/CD: запуск интеграционных тестов в GitHub Actions
Локальные тесты работают, теперь нужно настроить их выполнение в CI. Создадим workflow .github/workflows/test.yml:
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run integration tests
env:
DATABASE_URL: postgres://test_user:test_pass@localhost:5432/test_db
run: cargo test --test integration_test -- --nocapture
Обратите внимание:
- Docker-контейнер с БД запускается как сервис GitHub Actions — это быстрее, чем через
testcontainers. - Проверка здоровья контейнера (
health-cmd) гарантирует, что БД готова к подключению. - Переменная окружения
DATABASE_URLпередаётся в тесты.
Если вам нужно тестировать несколько сервисов (например, PostgreSQL + Redis), просто добавьте их в блок services.
5. Продвинутые техники: тестирование с моками и параллельный запуск
Иногда нужно протестировать взаимодействие с внешним API, которое нельзя поднять