Введение: Почему тестирование 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++ это может быть тестирование консольного приложения, сервера или встраиваемой системы.