Интеграционное тестирование C++ с Docker: практическое руководство

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

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

Начать курс

Интеграционное тестирование C++ с Docker

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

Почему Docker для интеграционного тестирования C++?

Традиционные подходы к интеграционному тестированию C++ часто сталкиваются с проблемами:

  • Зависимость от окружения: тесты могут работать на машине разработчика, но падать на сервере сборки из-за разных версий библиотек.
  • Сложность настройки: для тестирования с реальной базой данных или внешним API требуется ручная конфигурация.
  • Параллельный запуск: несколько тестовых прогонов могут конфликтовать из-за общих ресурсов.

Docker решает эти проблемы, предоставляя лёгкие, изолированные контейнеры. Вы можете поднять тестовую базу данных, запустить ваш C++ сервис и выполнить тесты — всё в рамках одного docker-compose файла. Это гарантирует, что окружение будет идентичным на любой машине.

Настройка проекта: базовый пример

Рассмотрим простой C++ сервис, который сохраняет данные в PostgreSQL. Для интеграционного тестирования нам понадобится:

  • Сам C++ код с тестами (используем Google Test).
  • Dockerfile для сборки и запуска тестов.
  • docker-compose.yml для оркестрации контейнеров.

Структура проекта:

my_cpp_project/
├── src/
│   ├── main.cpp
│   ├── database.cpp
│   └── database.h
├── tests/
│   └── test_integration.cpp
├── CMakeLists.txt
├── Dockerfile
└── docker-compose.yml

Dockerfile:

FROM gcc:latest AS builder

WORKDIR /app
COPY . .

RUN apt-get update && apt-get install -y cmake libpq-dev
RUN cmake -B build -DCMAKE_BUILD_TYPE=Release
RUN cmake --build build

FROM ubuntu:22.04

RUN apt-get update && apt-get install -y libpq-dev
COPY --from=builder /app/build/tests /app/tests
COPY --from=builder /app/build/my_app /app/my_app

ENTRYPOINT ["/app/tests"]

docker-compose.yml:

version: '3.8'
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
      POSTGRES_DB: testdb
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
      interval: 5s
      timeout: 5s
      retries: 5

  test:
    build: .
    depends_on:
      db:
        condition: service_healthy
    environment:
      DB_HOST: db
      DB_PORT: 5432
      DB_USER: testuser
      DB_PASSWORD: testpass
      DB_NAME: testdb

Написание интеграционных тестов на C++

Теперь создадим тест, который проверяет запись и чтение из базы данных. Используем Google Test и библиотеку libpqxx для работы с PostgreSQL.

tests/test_integration.cpp:

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

class DatabaseTest : public ::testing::Test {
protected:
    void SetUp() override {
        conn = new pqxx::connection(
            "host=" + std::string(std::getenv("DB_HOST")) +
            " port=" + std::string(std::getenv("DB_PORT")) +
            " user=" + std::string(std::getenv("DB_USER")) +
            " password=" + std::string(std::getenv("DB_PASSWORD")) +
            " dbname=" + std::string(std::getenv("DB_NAME"))
        );
        // Создаём таблицу для теста
        pqxx::work txn(*conn);
        txn.exec("CREATE TABLE IF NOT EXISTS test_data (id SERIAL PRIMARY KEY, value TEXT)");
        txn.commit();
    }

    void TearDown() override {
        pqxx::work txn(*conn);
        txn.exec("DROP TABLE IF EXISTS test_data");
        txn.commit();
        delete conn;
    }

    pqxx::connection* conn;
};

TEST_F(DatabaseTest, InsertAndRetrieve) {
    pqxx::work txn(*conn);
    txn.exec("INSERT INTO test_data (value) VALUES ('hello')");
    txn.commit();

    pqxx::work txn2(*conn);
    pqxx::result res = txn2.exec("SELECT value FROM test_data");
    EXPECT_EQ(res.size(), 1);
    EXPECT_EQ(res[0][0].as<std::string>(), "hello");
    txn2.commit();
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(MyCppProject)

find_package(PkgConfig REQUIRED)
pkg_check_modules(PQXX REQUIRED libpqxx)
pkg_check_modules(PQ REQUIRED libpq)

add_executable(tests tests/test_integration.cpp)
target_link_libraries(tests PRIVATE ${PQXX_LIBRARIES} ${PQ_LIBRARIES} gtest gtest_main)
target_include_directories(tests PRIVATE ${PQXX_INCLUDE_DIRS} ${PQ_INCLUDE_DIRS})

Интеграция с CI/CD (GitHub Actions)

Чтобы автоматизировать запуск тестов при каждом коммите, добавим workflow для GitHub Actions.

.github/workflows/test.yml:

name: Integration Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run integration tests
        run: |
          docker-compose -f docker-compose.yml up --build --abort-on-container-exit
      
      - name: Cleanup
        if: always()
        run: docker-compose down

Этот workflow поднимает контейнеры, ждёт готовности базы данных (благодаря healthcheck), запускает тесты и завершает работу. Флаг --abort-on-container-exit гарантирует, что пайплайн остановится при ошибке тестов.

Продвинутые

Блоги

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