Введение: Почему тестирование критически важно для C++ проектов
Тестирование кода на C++ — это не просто формальность, а фундаментальная практика, обеспечивающая стабильность, безопасность и поддерживаемость программного обеспечения. В отличие от интерпретируемых языков, где ошибка может проявиться мгновенно, в C++ баги часто остаются скрытыми до момента сборки или выполнения в специфических условиях. Пропущенная утечка памяти, неопределённое поведение или некорректная работа с указателями могут привести к фатальным сбоям.
Современная индустрия разработки выделяет три основных уровня тестирования: Unit (модульное), Integration (интеграционное) и E2E (сквозное). Каждый из них решает свою задачу и вместе они формируют "пирамиду тестирования", позволяющую поймать до 90% ошибок до выхода в production.
В этой статье мы разберём каждый тип тестов на практических примерах на C++, используя популярные фреймворки Google Test и Catch2, а также обсудим лучшие практики их применения.
1. Unit-тестирование: Проверка атомарных модулей
Unit-тесты — это самый нижний и самый быстрый уровень. Они проверяют отдельные функции, классы или методы в изоляции от остальной системы. Цель — убедиться, что каждый "кирпичик" кода работает корректно.
Пример Unit-теста с Google Test
Рассмотрим простую математическую функцию:
// math_utils.h
#pragma once
namespace MathUtils {
int add(int a, int b);
int divide(int a, int b);
}
// math_utils.cpp
#include "math_utils.h"
namespace MathUtils {
int add(int a, int b) {
return a + b;
}
int divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}
}
Теперь напишем unit-тесты с помощью Google Test:
// test_math_utils.cpp
#include <gtest/gtest.h>
#include "math_utils.h"
TEST(MathUtilsTest, AddPositiveNumbers) {
EXPECT_EQ(MathUtils::add(2, 3), 5);
EXPECT_EQ(MathUtils::add(0, 0), 0);
}
TEST(MathUtilsTest, AddNegativeNumbers) {
EXPECT_EQ(MathUtils::add(-1, -1), -2);
EXPECT_EQ(MathUtils::add(-5, 3), -2);
}
TEST(MathUtilsTest, DivideNormal) {
EXPECT_EQ(MathUtils::divide(10, 2), 5);
EXPECT_EQ(MathUtils::divide(7, 3), 2); // Целочисленное деление
}
TEST(MathUtilsTest, DivideByZeroThrows) {
EXPECT_THROW(MathUtils::divide(5, 0), std::invalid_argument);
}
Использование Mock-объектов
Для изоляции модуля часто требуется подменить зависимости. Google Mock (часть Google Test) позволяет создавать заглушки:
// database_interface.h
class DatabaseInterface {
public:
virtual ~DatabaseInterface() = default;
virtual bool save(const std::string& data) = 0;
};
// user_service.cpp
class UserService {
DatabaseInterface& db;
public:
explicit UserService(DatabaseInterface& db) : db(db) {}
bool registerUser(const std::string& name) {
return db.save("user:" + name);
}
};
// test_user_service.cpp
#include <gmock/gmock.h>
class MockDatabase : public DatabaseInterface {
public:
MOCK_METHOD(bool, save, (const std::string&), (override));
};
TEST(UserServiceTest, RegisterUserCallsDb) {
MockDatabase mockDb;
EXPECT_CALL(mockDb, save("user:Alice")).WillOnce(testing::Return(true));
UserService service(mockDb);
EXPECT_TRUE(service.registerUser("Alice"));
}
Плюсы: Скорость выполнения (миллисекунды), точная локализация ошибок, возможность запускать без внешних зависимостей.
Минусы: Не проверяют взаимодействие между реальными компонентами.
2. Интеграционное тестирование: Проверка связки компонентов
Integration-тесты проверяют, как несколько модулей работают вместе. В C++ это часто включает взаимодействие с базами данных, файловой системой, сетевыми сокетами или другими библиотеками.
Пример: Тестирование работы с файловой системой
// file_manager.h
#include <string>
#include <fstream>
class FileManager {
public:
bool writeToFile(const std::string& path, const std::string& content);
std::string readFromFile(const std::string& path);
};
// file_manager.cpp
bool FileManager::writeToFile(const std::string& path, const std::string& content) {
std::ofstream file(path);
if (!file.is_open()) return false;
file << content;
return true;
}
std::string FileManager::readFromFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) return "";
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
return content;
}
Интеграционный тест (создаёт реальный файл на диске):
// integration_test_file.cpp
#include <gtest/gtest.h>
#include <filesystem>
#include "file_manager.h"
namespace fs = std::filesystem;
TEST(FileManagerIntegration, WriteAndReadFile) {
const std::string testPath = "test_integration.txt";
const std::string testContent = "Hello, Integration Test!";
FileManager fm;
// Запись
ASSERT_TRUE(fm.writeToFile(testPath, testContent));
// Чтение
std::string result = fm.readFromFile(testPath);
EXPECT_EQ(result, testContent);
// Очистка
fs::remove(testPath);
}
TEST(FileManagerIntegration, ReadNonExistentFile) {
FileManager fm;
EXPECT_EQ(fm.readFromFile("nonexistent.txt"), "");
}
Важно: Интеграционные тесты должны использовать временные ресурсы (тестовые БД, временные папки) и очищать их после себя.
Пример с базой данных (SQLite)
// integration_test_db.cpp
#include <gtest/gtest.h>
#include <sqlite3.h>
class DatabaseManager {
sqlite3* db;
public:
bool open(const std::string& path) {
return sqlite3_open(path.c_str(), &db) == SQLITE_OK;
}
bool execute(const std::string& sql) {
return sqlite3_exec(db, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK;
}
void close() { sqlite3_close(db); }
};
TEST(DatabaseIntegration, CreateTableAndInsert) {
DatabaseManager db;
ASSERT_TRUE(db.open(":memory:")); // in-memory БД
EXPECT_TRUE(db.execute("CREATE TABLE test (id