Тестирование в C++: unit, integration, e2e от А до Я — полное руководство

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

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

Начать курс

Введение: Почему тестирование C++ — это база надёжного кода

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

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

1. Unit-тестирование в C++: Тестируем «кирпичики» программы

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

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

На рынке C++ существует несколько зрелых фреймворков. Вот самые популярные:

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

Мы будем использовать Google Test как самый распространённый вариант.

1.2. Пример unit-теста на C++ (Google Test)

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

// math_utils.h
#pragma once
int factorial(int n);
// math_utils.cpp
#include "math_utils.h"
int factorial(int n) {
    if (n < 0) return -1; // ошибка: отрицательное число
    if (n == 0) return 1;
    return n * factorial(n - 1);
}

Теперь напишем 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(5), 120);
}

TEST(FactorialTest, HandlesNegativeInput) {
    EXPECT_EQ(factorial(-1), -1);
}

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

Запускаем тесты — и видим зелёный свет. Если бы в функции была ошибка (например, factorial(5) возвращал 119), тест бы упал, и мы бы сразу узнали о проблеме.

1.3. Best practices для unit-тестов

  • Изоляция: Используйте моки (Mock) для замены внешних зависимостей (БД, сеть, файловая система).
  • Одна проверка на тест: Каждый TEST должен проверять одно поведение.
  • Понятные имена: HandlesZeroInput лучше, чем Test1.
  • Покрытие граничных случаев: Ноль, отрицательные числа, максимумы.

2. Интеграционное тестирование (Integration Testing): Проверяем взаимодействие

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

2.1. Когда нужно интеграционное тестирование?

  • Когда два модуля обмениваются данными (например, парсер + валидатор).
  • При работе с внешними ресурсами: база данных, файлы, сетевые сокеты.
  • После объединения нескольких unit-тестированных компонентов.

2.2. Пример интеграционного теста на C++

Представим, что у нас есть класс DatabaseManager и класс UserService, который его использует. Вместо мока мы поднимаем реальную тестовую БД (например, SQLite в памяти):

// test_user_service.cpp
#include <gtest/gtest.h>
#include "UserService.h"
#include "DatabaseManager.h"

class UserServiceIntegrationTest : public ::testing::Test {
protected:
    DatabaseManager* db;
    UserService* service;

    void SetUp() override {
        db = new DatabaseManager(":memory:"); // in-memory SQLite
        db->createTables();
        service = new UserService(db);
    }

    void TearDown() override {
        delete service;
        delete db;
    }
};

TEST_F(UserServiceIntegrationTest, RegisterAndFindUser) {
    service->registerUser("test@example.com", "password123");
    auto user = service->findUserByEmail("test@example.com");
    ASSERT_NE(user, nullptr);
    EXPECT_EQ(user->email, "test@example.com");
}

Здесь мы проверяем, что UserService корректно взаимодействует с DatabaseManager — данные сохраняются и извлекаются.

2.3. Особенности интеграционных тестов

  • Медленнее unit-тестов: Запуск БД, сети требует времени.
  • Хрупкость: Могут падать из-за внешних факторов (нет сети, сменился API).
  • Чистота: Используйте отдельные тестовые базы данных или контейнеры (Docker).

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

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

3.1. Инструменты для E2E тестирования C++

Блоги

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