Интеграционное тестирование Rust с Docker: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Интеграционное тестирование 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, которое нельзя поднять

Блоги

Рекомендаций по книгам