Интеграционное тестирование Rust с Docker: продвинутый уровень

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

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

Начать курс

Интеграционное тестирование 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                

Блоги

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