Интеграционное тестирование C++ с Docker: от А до Я
Интеграционное тестирование — ключевой этап разработки любого серьезного C++ проекта. Оно позволяет проверить, как отдельные модули системы работают вместе, взаимодействуют с базами данных, сетевыми сервисами и файловыми системами. Docker в этом контексте становится незаменимым инструментом: он обеспечивает изолированное, воспроизводимое окружение, исключая проблемы с "оно работает на моей машине". В этой статье мы разберем полный цикл настройки интеграционного тестирования C++ с Docker — от базовых концепций до продвинутых практик.
1. Почему Docker для интеграционного тестирования C++?
Традиционные интеграционные тесты C++ часто страдают от проблем окружения: разные версии библиотек, несовместимые компиляторы, специфичные настройки ОС. Docker решает эти проблемы раз и навсегда. Основные преимущества:
- Воспроизводимость: каждый тест запускается в идентичном окружении, независимо от хост-системы.
- Изоляция: контейнеры не влияют друг на друга и на хост-систему.
- Масштабируемость: легко запускать сотни тестов параллельно.
- CI/CD интеграция: контейнеры идеально вписываются в пайплайны Jenkins, GitLab CI, GitHub Actions.
Рассмотрим простой пример: ваше C++ приложение использует PostgreSQL. Без Docker вам пришлось бы устанавливать и настраивать PostgreSQL на каждом тестовом сервере. С Docker это одна строка конфигурации.
2. Настройка Docker-окружения для C++ проекта
Начнем с создания Dockerfile для нашего C++ приложения. Предположим, проект использует CMake и Google Test для unit-тестов, но нам нужны интеграционные тесты с реальной базой данных.
Базовый Dockerfile для сборки и тестирования:
# Dockerfile
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
git \
libpq-dev \
libgtest-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release \
&& cmake --build build --parallel
# Стадия для тестов
FROM builder AS tester
CMD ["./build/integration_tests"]
Docker Compose для запуска инфраструктуры:
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 5s
retries: 5
app:
build: .
depends_on:
db:
condition: service_healthy
environment:
DB_HOST: db
DB_PORT: 5432
DB_NAME: testdb
DB_USER: testuser
DB_PASSWORD: testpass
command: ["./build/integration_tests"]
3. Написание интеграционных тестов на C++
Рассмотрим пример интеграционного теста, который проверяет взаимодействие с PostgreSQL через библиотеку libpqxx. Предположим, у нас есть модуль для работы с пользователями.
Пример интеграционного теста:
// integration_tests.cpp
#include <gtest/gtest.h>
#include <pqxx/pqxx>
#include <string>
#include <vector>
class DatabaseIntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
conn = new pqxx::connection(
"host=" + std::getenv("DB_HOST") +
" port=" + std::getenv("DB_PORT") +
" dbname=" + std::getenv("DB_NAME") +
" user=" + std::getenv("DB_USER") +
" password=" + std::getenv("DB_PASSWORD")
);
// Создаем тестовую таблицу
pqxx::work txn(*conn);
txn.exec("CREATE TABLE IF NOT EXISTS users ("
"id SERIAL PRIMARY KEY, "
"name VARCHAR(100), "
"email VARCHAR(100) UNIQUE)");
txn.commit();
}
void TearDown() override {
// Очищаем данные после тестов
pqxx::work txn(*conn);
txn.exec("DROP TABLE IF EXISTS users CASCADE");
txn.commit();
delete conn;
}
pqxx::connection* conn;
};
// Функция, которую тестируем
bool addUser(pqxx::connection& conn,
const std::string& name,
const std::string& email) {
try {
pqxx::work txn(conn);
txn.exec_params(
"INSERT INTO users (name, email) VALUES ($1, $2)",
name, email
);
txn.commit();
return true;
} catch (const std::exception& e) {
return false;
}
}
TEST_F(DatabaseIntegrationTest, AddUserSuccess) {
ASSERT_TRUE(addUser(*conn, "John Doe", "john@example.com"));
pqxx::work txn(*conn);
auto result = txn.exec("SELECT COUNT(*) FROM users");
ASSERT_EQ(result[0][0].as<int>(), 1);
}
TEST_F(DatabaseIntegrationTest, AddDuplicateEmail) {
addUser(*conn, "John", "john@example.com");
ASSERT_FALSE(addUser(*conn, "Jane", "john@example.com"));
}
TEST_F(DatabaseIntegrationTest, EmptyDatabase) {
pqxx::work txn(*conn);
auto result = txn.exec("SELECT COUNT(*) FROM users");
ASSERT_EQ(result[0][0].as<int>(), 0);
}
4. Запуск тестов и интеграция с CI/CD
Теперь, когда у нас есть Docker-окружение и тесты, настроим их запуск. Создадим Makefile для удобства:
# Makefile
.PHONY: build test clean
build:
docker compose build
test:
docker compose up --abort-on-container-exit --exit-code-from app
clean:
docker compose down -v
Интеграция с GitHub Actions:
# .github/workflows/integration-tests.yml
name: Integration Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and run integration tests
run: |
docker compose build
docker compose up --abort-on-container-exit --exit-code-from app
- name: Cleanup
if: always()
run: docker compose down -v
5. Лучшие практики и советы
Чтобы интеграционное те