Тестирование в C++: Unit, Integration, E2E от А до Я

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

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

Начать курс

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

В мире C++ качество кода имеет решающее значение. Язык даёт разработчику огромную власть над памятью и производительностью, но эта власть требует высокой ответственности. Ошибка в управлении памятью, неверное использование указателей или некорректная работа с многопоточностью могут привести к трудноуловимым багам, которые проявляются только в production.

Тестирование — это единственный надёжный способ гарантировать, что ваш C++ код ведёт себя так, как задумано. В этой статье мы пройдём путь от маленьких unit-тестов до полных end-to-end (E2E) проверок. Вы узнаете, как правильно выстроить стратегию тестирования, какие инструменты использовать и как писать тесты, которые действительно приносят пользу.

Мы рассмотрим три уровня пирамиды тестирования: unit (модульное), integration (интеграционное) и e2e (сквозное). Каждый из них решает свою задачу, и только их комбинация даёт полную уверенность в надёжности программы.

Unit-тестирование: Фундамент качества

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

Выбор фреймворка для C++

В экосистеме C++ есть несколько популярных фреймворков для unit-тестирования:

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

Для примера будем использовать Google Test, так как это стандарт де-факто во многих C++ проектах.

Пример unit-теста на C++

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

// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

long long factorial(int n);

#endif

// math_utils.cpp
#include "math_utils.h"

long long factorial(int n) {
    if (n < 0) throw std::invalid_argument("Negative input");
    if (n == 0) return 1;
    long long result = 1;
    for (int i = 1; i <= n; ++i) {
        result *= i;
    }
    return result;
}

Теперь напишем unit-тест с использованием Google Test:

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

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

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

TEST(FactorialTest, ThrowsOnNegative) {
    EXPECT_THROW(factorial(-1), std::invalid_argument);
}

Этот тест проверяет три сценария: граничный случай (0), нормальную работу и обработку ошибки. Обратите внимание на макросы EXPECT_EQ и EXPECT_THROW — они не прерывают тест при ошибке, а лишь фиксируют её.

Mock-объекты для изоляции

Для unit-тестирования критически важно изолировать тестируемый код от внешних зависимостей. Для этого используются mock-объекты. Google Mock (часть Google Test) позволяет легко создавать заглушки:

// database_connector.h
class DatabaseConnector {
public:
    virtual ~DatabaseConnector() = default;
    virtual bool connect(const std::string& url) = 0;
    virtual std::vector<int> fetchData() = 0;
};

// user_service.cpp
class UserService {
    DatabaseConnector& db;
public:
    UserService(DatabaseConnector& db) : db(db) {}
    int getUserCount() {
        auto data = db.fetchData();
        return data.size();
    }
};

// test_user_service.cpp
#include <gmock/gmock.h>

class MockDatabase : public DatabaseConnector {
public:
    MOCK_METHOD(bool, connect, (const std::string&), (override));
    MOCK_METHOD(std::vector<int>, fetchData, (), (override));
};

TEST(UserServiceTest, ReturnsCorrectCount) {
    MockDatabase mockDb;
    EXPECT_CALL(mockDb, fetchData())
        .WillOnce(testing::Return(std::vector<int>{1, 2, 3}));
    
    UserService service(mockDb);
    EXPECT_EQ(service.getUserCount(), 3);
}

Теперь мы тестируем только логику UserService, не подключаясь к реальной базе данных.

Интеграционное тестирование: Проверка взаимодействия компонентов

Интеграционные тесты проверяют, как разные модули системы работают вместе. В отличие от unit-тестов, здесь мы уже не изолируемся от всех зависимостей — мы проверяем реальное взаимодействие, например, с базой данных, API или файловой системой.

Стратегия интеграционного тестирования в C++

  • Тестирование API: вызовы функций одного модуля из другого.
  • Работа с внешними ресурсами: подключение к тестовой БД, чтение/запись файлов.
  • Проверка контрактов: убедиться, что модули обмениваются данными в ожидаемом формате.

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

Предположим, у нас есть класс FileProcessor, который читает данные из файла, и класс DataAnalyzer, который их обрабатывает. Интеграционный тест проверит их совместную работу:

// file_processor.h
class FileProcessor {
public:
    std::vector<std::string> readLines(const std::string& path);
};

// data_analyzer.h
class DataAnalyzer {
public:
    int countWords(const std::vector<std::string>& lines);
};

// integration_test.cpp
#include <gtest/gtest.h>
#include "file_processor.h"
#include "data_analyzer.h"

TEST(IntegrationTest, FileToAnalyzerPipeline) {
    // Создаём временный файл
    std::ofstream testFile("test_data.txt");
    testFile &                

Блоги

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