Интеграционное тестирование C++ с Docker: продвинутый уровень
Разработка современных C++ приложений всё чаще переходит в парадигму микросервисов и контейнеризации. Интеграционное тестирование (Integration Testing) становится критическим этапом, позволяющим проверить взаимодействие компонентов системы в условиях, максимально приближенных к боевым. Docker предоставляет идеальную среду для изолированного и воспроизводимого тестирования. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования C++ с использованием Docker, включая настройку контейнеров, написание тестов с Google Test и интеграцию в пайплайны CI/CD.
1. Почему Docker — идеальный инструмент для интеграционного тестирования C++?
Традиционное юнит-тестирование проверяет отдельные функции или классы, но не гарантирует корректной работы системы в целом. Интеграционные тесты проверяют связки: базы данных, сетевые протоколы, файловую систему, внешние API. Здесь Docker становится незаменимым:
- Воспроизводимость: Docker-образ содержит точную копию окружения (компилятор, библиотеки, переменные окружения). Тест, пройденный локально, пройдёт на сервере CI и у другого разработчика.
- Изоляция: Каждый тест запускается в чистом контейнере. Нет "загрязнения" глобальными настройками ОС или остаточными данными от предыдущих тестов.
- Параллелизм: Можно запускать множество тестовых контейнеров одновременно, экономя время в CI/CD.
- Управление зависимостями: Docker Compose позволяет поднять целую инфраструктуру: PostgreSQL, Redis, Kafka — за несколько секунд.
2. Настройка тестового окружения с Docker Compose
Рассмотрим типичный сценарий: C++ сервис, который читает данные из PostgreSQL и отправляет их в RabbitMQ. Нам нужно протестировать интеграцию этих трёх компонентов.
Создадим docker-compose.test.yml:
version: '3.8'
services:
postgres_test:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 5s
retries: 5
rabbitmq_test:
image: rabbitmq:3-management-alpine
environment:
RABBITMQ_DEFAULT_USER: testuser
RABBITMQ_DEFAULT_PASS: testpass
ports:
- "5672:5672"
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
interval: 5s
timeout: 5s
retries: 5
test_runner:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
postgres_test:
condition: service_healthy
rabbitmq_test:
condition: service_healthy
environment:
DB_HOST: postgres_test
DB_PORT: 5432
DB_NAME: testdb
DB_USER: testuser
DB_PASS: testpass
RMQ_HOST: rabbitmq_test
RMQ_PORT: 5672
Ключевой момент — healthcheck. Мы не запускаем тесты, пока база данных и брокер сообщений не будут готовы к работе. Это предотвращает "race condition" в тестах.
3. Написание интеграционных тестов на C++ с Google Test
Для демонстрации используем Google Test (gtest) — стандарт де-факто для тестирования C++. Напишем тест, который проверяет вставку данных в PostgreSQL и их чтение через RabbitMQ.
Сначала создадим простую обёртку для работы с RabbitMQ (упрощённо):
// rabbitmq_client.h
#pragma once
#include <string>
#include <amqpcpp.h>
class RabbitMQClient {
public:
RabbitMQClient(const std::string& host, int port,
const std::string& user, const std::string& pass);
bool sendMessage(const std::string& queue, const std::string& message);
std::string receiveMessage(const std::string& queue, int timeout_ms);
};
Теперь напишем интеграционный тест:
// integration_test.cpp
#include <gtest/gtest.h>
#include <pqxx/pqxx> // libpqxx для PostgreSQL
#include "rabbitmq_client.h"
class IntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
// Подключение к PostgreSQL
conn = std::make_unique<pqxx::connection>(
"host=" + db_host +
" port=" + db_port +
" dbname=" + db_name +
" user=" + db_user +
" password=" + db_pass
);
// Создаём тестовую таблицу
pqxx::work txn(*conn);
txn.exec("CREATE TABLE IF NOT EXISTS test_data ("
"id SERIAL PRIMARY KEY, "
"value TEXT NOT NULL)");
txn.commit();
// Подключение к RabbitMQ
rmq = std::make_unique<RabbitMQClient>(
rmq_host, std::stoi(rmq_port),
rmq_user, rmq_pass
);
}
void TearDown() override {
// Очистка: удаляем таблицу и отключаемся
pqxx::work txn(*conn);
txn.exec("DROP TABLE IF EXISTS test_data");
txn.commit();
conn->close();
}
std::unique_ptr<pqxx::connection> conn;
std::unique_ptr<RabbitMQClient> rmq;
const std::string db_host = std::getenv("DB_HOST") ?: "localhost";
const std::string db_port = std::getenv("DB_PORT") ?: "5432";
const std::string db_name = std::getenv("DB_NAME") ?: "testdb";
const std::string db_user = std::getenv("DB_USER") ?: "testuser";
const std::string db_pass = std::getenv("DB_PASS") ?: "testpass";
const std::string rmq_host = std::getenv("RMQ_HOST") ?: "localhost";
const std::string rmq_port = std::getenv("RMQ_PORT") ?: "5672";
const std::string rmq_user = std::getenv("RMQ_USER") ?: "testuser";
const std::string rmq_pass = std::getenv("RMQ_PASS") ?: "testpass";
};
TEST_F(IntegrationTest, InsertAndPublish) {
// Шаг 1: Вставляем данные в БД
pqxx::work txn(*conn);
txn.exec_params("INSERT INTO test_data (value) VALUES ($1)", "hello_docker");