Интеграционное тестирование 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 {}
Для управления состоянием между тестами используйте:
- Транза