Тестирование в C++: Unit, Integration и E2E. Полное руководство

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

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

Начать курс

Введение: Почему тестирование в C++ — это не роскошь, а необходимость

Разработка на C++ — это всегда баланс между производительностью и надёжностью. Ошибки управления памятью, неопределённое поведение (UB) и сложные зависимости между модулями делают C++ одним из самых требовательных языков к качеству кода. Без системного подхода к тестированию проект быстро превращается в «чёрный ящик», где исправление одного бага порождает два новых.

В этой статье мы разберём три ключевых уровня тестирования в C++: unit-тесты (модульные), integration-тесты (интеграционные) и e2e-тесты (сквозные). Вы узнаете, какие инструменты использовать, как писать эффективные тесты и как выстроить пирамиду тестирования для C++-проекта.

1. Unit-тестирование: Проверяем каждый кирпичик

Unit-тесты — это основа пирамиды тестирования. Они изолированно проверяют одну логическую единицу кода: функцию, метод класса или небольшую структуру. В C++ unit-тесты должны выполняться быстро (<1 секунды на сотни тестов) и не зависеть от внешних ресурсов (файловая система, сеть, базы данных).

Инструменты для unit-тестирования C++

  • Google Test (gtest) — самый популярный фреймворк от Google. Поддерживает assertion-макросы, fixtures и параметризованные тесты.
  • Catch2 — современная альтернатива с лаконичным синтаксисом и поддержкой BDD-стиля.
  • Boost.Test — часть библиотеки Boost, зрелый инструмент с широкими возможностями.

Пример unit-теста на Google Test

Допустим, у нас есть функция расчёта факториала:

// math_utils.h
#pragma once
unsigned long long factorial(unsigned int n);
// math_utils.cpp
#include "math_utils.h"

unsigned long long factorial(unsigned int n) {
    if (n == 0) return 1;
    unsigned long long result = 1;
    for (unsigned int i = 1; i <= n; ++i) {
        result *= i;
    }
    return result;
}

Пишем unit-тест:

// test_math_utils.cpp
#include <gtest/gtest.h>
#include "math_utils.h"

TEST(FactorialTest, HandlesZero) {
    EXPECT_EQ(factorial(0), 1);
}

TEST(FactorialTest, HandlesPositiveNumbers) {
    EXPECT_EQ(factorial(1), 1);
    EXPECT_EQ(factorial(5), 120);
    EXPECT_EQ(factorial(10), 3628800);
}

TEST(FactorialTest, HandlesLargeInput) {
    // 20! умещается в unsigned long long
    EXPECT_EQ(factorial(20), 2432902008176640000ULL);
}

Лучшие практики unit-тестирования в C++:

  • Один тест — одна проверка. Используйте EXPECT_EQ для мягких и ASSERT_EQ для критических проверок.
  • Изолируйте зависимости через mock-объекты (Google Mock).
  • Тестируйте граничные случаи: нулевые значения, переполнение, пустые контейнеры.
  • Используйте TEST_F для повторяющейся инициализации (fixtures).

2. Интеграционное тестирование: Как модули работают вместе

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

В отличие от unit-тестов, интеграционные тесты могут работать с реальными базами данных, файлами или сетевыми сокетами, но обычно в тестовом окружении (например, временные файлы или Docker-контейнеры).

Пример интеграционного теста

Предположим, у нас есть модуль Logger и модуль FileManager. Мы хотим проверить, что логи корректно пишутся в файл.

// file_logger.h
#pragma once
#include <string>

class FileLogger {
public:
    FileLogger(const std::string& filename);
    void log(const std::string& message);
    std::string readLastLine() const;
private:
    std::string filename_;
};
// test_file_logger.cpp
#include <gtest/gtest.h>
#include <fstream>
#include <filesystem>
#include "file_logger.h"

namespace fs = std::filesystem;

class FileLoggerIntegrationTest : public ::testing::Test {
protected:
    void SetUp() override {
        test_file_ = "test_log_" + std::to_string(rand()) + ".txt";
        logger_ = std::make_unique<FileLogger>(test_file_);
    }

    void TearDown() override {
        if (fs::exists(test_file_)) {
            fs::remove(test_file_);
        }
    }

    std::string test_file_;
    std::unique_ptr<FileLogger> logger_;
};

TEST_F(FileLoggerIntegrationTest, WritesAndReadsMessage) {
    logger_->log("Integration test message");
    std::string line = logger_->readLastLine();
    EXPECT_EQ(line, "Integration test message");
}

TEST_F(FileLoggerIntegrationTest, MultipleMessages) {
    logger_->log("First");
    logger_->log("Second");
    EXPECT_EQ(logger_->readLastLine(), "Second");
}

Советы по интеграционному тестированию C++:

  • Используйте временные файлы и ресурсы, очищайте их в TearDown().
  • Для тестирования сети применяйте mock-серверы (например, cpp-httplib в тестовом режиме).
  • Запускайте интеграционные тесты отдельно от unit-тестов (разные сьюиты или бинарники).
  • Документируйте, какие внешние зависимости нужны для запуска (БД, API).

3. E2E-тестирование: Проверяем систему целиком

Сквозные (end-to-end) тесты имитируют поведение реального пользователя или внешней системы. Для C++ это может быть:

  • Запуск собранного приложения с тестовыми входными данными.
  • Проверка REST API (если это сервер на C++).
  • Тестирование CLI-инструмента с различными аргументами.

E2E

Блоги

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