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

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

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

Начать курс

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

Интеграционное тестирование — критически важный этап разработки на C++, особенно в современных микросервисных архитектурах. Традиционные подходы, когда тесты выполняются в среде разработчика, часто приводят к ошибкам из-за различий в окружении, версиях библиотек и конфигурациях. Docker предлагает элегантное решение: полная изоляция, воспроизводимость и автоматизация. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования C++ приложений с использованием Docker, включая управление зависимостями, параллельный запуск тестов и интеграцию с CI/CD.

1. Архитектура тестового окружения на Docker

Первый шаг к надежному интеграционному тестированию — правильная архитектура Docker-окружения. Вместо одного монолитного контейнера мы создаем композицию сервисов, каждый из которых отвечает за свою задачу: сам тестируемый сервис, база данных, кэш, очереди сообщений и т.д.

Рассмотрим пример для C++ микросервиса, который использует PostgreSQL и Redis. Файл docker-compose.test.yml:

version: '3.8'

services:
  postgres_test:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: test_user
      POSTGRES_PASSWORD: test_pass
      POSTGRES_DB: test_db
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test_user -d test_db"]
      interval: 2s
      timeout: 5s
      retries: 10

  redis_test:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 5s
      retries: 10

  cpp_service_test:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      postgres_test:
        condition: service_healthy
      redis_test:
        condition: service_healthy
    environment:
      - DB_HOST=postgres_test
      - DB_PORT=5432
      - REDIS_HOST=redis_test
      - REDIS_PORT=6379
      - TEST_MODE=1
    volumes:
      - ./test_results:/app/test_results
    command: ["./run_integration_tests"]

Ключевые моменты:

  • Healthcheck'и гарантируют, что тесты запустятся только после готовности зависимостей.
  • Отдельная сеть (создается автоматически) изолирует тестовое окружение от продакшена.
  • Том для результатов позволяет забрать отчеты о тестировании после завершения.

2. Написание интеграционных тестов на C++ с учетом Docker

Ваши тесты должны быть готовы к работе в контейнеризованной среде. Это означает, что конфигурация подключения к внешним сервисам должна задаваться через переменные окружения, а не хардкодиться.

Пример теста с использованием Google Test и libpqxx для PostgreSQL:

#include <gtest/gtest.h>
#include <pqxx/pqxx>
#include <cstdlib>

class DatabaseIntegrationTest : public ::testing::Test {
protected:
    std::unique_ptr<pqxx::connection> conn;

    void SetUp() override {
        const char* db_host = std::getenv("DB_HOST");
        const char* db_port = std::getenv("DB_PORT");
        const char* db_user = std::getenv("POSTGRES_USER");
        const char* db_pass = std::getenv("POSTGRES_PASSWORD");
        const char* db_name = std::getenv("POSTGRES_DB");

        ASSERT_NE(db_host, nullptr) << "DB_HOST not set";
        ASSERT_NE(db_port, nullptr) << "DB_PORT not set";

        std::string conn_str = "host=" + std::string(db_host) +
                               " port=" + std::string(db_port) +
                               " user=" + std::string(db_user ? db_user : "test_user") +
                               " password=" + std::string(db_pass ? db_pass : "test_pass") +
                               " dbname=" + std::string(db_name ? db_name : "test_db");

        try {
            conn = std::make_unique<pqxx::connection>(conn_str);
            ASSERT_TRUE(conn->is_open()) << "Failed to connect to database";
        } catch (const std::exception& e) {
            FAIL() << "Connection error: " << e.what();
        }
    }

    void TearDown() override {
        if (conn && conn->is_open()) {
            conn->close();
        }
    }
};

TEST_F(DatabaseIntegrationTest, CreateAndQueryUser) {
    pqxx::work txn(*conn);

    // Создаем таблицу и вставляем данные
    txn.exec("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)");
    txn.exec("INSERT INTO users (name) VALUES ('Alice')");

    // Запрашиваем данные
    pqxx::result result = txn.exec("SELECT name FROM users WHERE id = 1");
    ASSERT_EQ(result.size(), 1);
    EXPECT_EQ(result[0][0].as<std::string>(), "Alice");

    txn.commit();
}

Такой подход гарантирует, что тесты будут работать одинаково на локальной машине разработчика и в CI/CD пайплайне.

3. Параллельный запуск тестов и управление состоянием

Одна из главных проблем интеграционных тестов — их длительность. Docker позволяет легко организовать параллельный запуск тестов, используя изолированные контейнеры для каждого набора тестов.

Рассмотрим продвинутую технику: запуск тестов в нескольких экземплярах Docker Compose с разными портами.

#!/bin/bash
# parallel_test_runner.sh

set -e

NUM_INSTANCES=4
BASE_PORT=5432
COMPOSE_FILE="docker-compose.test.yml"

run_test_instance() {
    local instance_id=$1
    local db_port=$((BASE_PORT + instance_id))
    local project_name="test_${instance_id}"

    echo "Starting instance ${instance_id} on port ${db_port}..."

    # Запускаем изолированный экземпляр с уникальным портом
    DB_PORT=${db_port} docker-compose -p ${project_name} \
        -f ${COMPOSE_FILE} up --abort-on-container-exit --exit-code-from cpp_service_test

    local exit_code=$?
    echo "Instance ${instance_id} finished with exit code ${exit_code}"

    # Очищаем контейнеры
    docker-compose -p ${project_name} -f ${COMPOSE_FILE} down -v

    return ${exit_code}
}

# Экспортируем функцию для parallel
export -f run_test_instance

# Запускаем параллельно
seq 1 ${NUM_INSTANCES} | parallel -j ${NUM_INSTANCES} run_test_instance {}

Для управления состоянием между тестами используйте:

  • Транза

Блоги

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