Тестирование в C++: Unit, Integration и E2E тесты с примерами

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

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

Начать курс

Введение: Почему тестирование критически важно для 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                

Блоги

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