Тестирование в C++: unit, integration, e2e — продвинутый уровень

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

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

Начать курс

Продвинутое тестирование C++: от юнит-тестов до end-to-end

В современной C++ разработке качество кода определяется не только его производительностью, но и надёжностью. Продвинутое тестирование выходит далеко за рамки простых проверок — это архитектурный подход, включающий юнит-тесты, интеграционные и end-to-end (e2e) тесты. В этой статье мы разберём, как правильно выстроить пирамиду тестирования для C++ проектов, используя Google Test, Google Mock и собственные фреймворки.

1. Юнит-тестирование: изоляция и моки

Юнит-тесты проверяют отдельные модули (функции, классы) в изоляции. На продвинутом уровне ключевая задача — научиться правильно mock-ить зависимости. Рассмотрим пример с использованием Google Mock.

Допустим, у нас есть класс DatabaseConnector, который зависит от внешнего API. Мы хотим протестировать логику обработки данных, не обращаясь к реальной базе.

#include <gmock/gmock.h>
#include <gtest/gtest.h>

class DatabaseInterface {
public:
    virtual ~DatabaseInterface() = default;
    virtual std::string fetchData(int id) = 0;
};

class MockDatabase : public DatabaseInterface {
public:
    MOCK_METHOD(std::string, fetchData, (int id), (override));
};

class DataProcessor {
    DatabaseInterface& db;
public:
    explicit DataProcessor(DatabaseInterface& db) : db(db) {}
    std::string process(int id) {
        auto raw = db.fetchData(id);
        if (raw.empty()) return "EMPTY";
        return "PROCESSED: " + raw;
    }
};

TEST(DataProcessorTest, ProcessWithValidData) {
    MockDatabase mockDb;
    EXPECT_CALL(mockDb, fetchData(42))
        .WillOnce(testing::Return("Hello"));
    DataProcessor processor(mockDb);
    EXPECT_EQ(processor.process(42), "PROCESSED: Hello");
}

Использование MOCK_METHOD позволяет точно контролировать поведение зависимостей. Важно помнить про strict mocks (через NiceMock или StrictMock) для отлова неожиданных вызовов.

2. Интеграционное тестирование: тестирование взаимодействий

Интеграционные тесты проверяют, как модули работают вместе. В C++ это часто включает тестирование работы с файловой системой, сетевыми протоколами или базами данных. Продвинутый подход — использовать test fixtures и parameterized tests.

Пример: тестируем класс FileManager, который читает и пишет в файлы. Используем временные директории.

class FileManagerTest : public ::testing::Test {
protected:
    std::filesystem::path tempDir;
    void SetUp() override {
        tempDir = std::filesystem::temp_directory_path() / "test_files";
        std::filesystem::create_directories(tempDir);
    }
    void TearDown() override {
        std::filesystem::remove_all(tempDir);
    }
};

TEST_F(FileManagerTest, WriteAndReadFile) {
    FileManager fm(tempDir.string());
    fm.write("test.txt", "data");
    auto content = fm.read("test.txt");
    EXPECT_EQ(content, "data");
}

Для параметризованных тестов используем TEST_P с INSTANTIATE_TEST_SUITE_P. Это позволяет прогнать один и тот же тест с разными входными данными.

class MathTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};
TEST_P(MathTest, Add) {
    auto [a, b, expected] = GetParam();
    EXPECT_EQ(add(a, b), expected);
}
INSTANTIATE_TEST_SUITE_P(AddTests, MathTest,
    ::testing::Values(
        std::make_tuple(1, 1, 2),
        std::make_tuple(-1, 1, 0),
        std::make_tuple(0, 0, 0)
    ));

3. End-to-End тестирование: проверка полного цикла

E2e тесты имитируют поведение конечного пользователя. Для C++ приложений это может означать запуск полного бинарника, проверку его вывода или взаимодействие через API. Продвинутый уровень — автоматизация через CI/CD и контейнеризация.

Пример: тестируем консольное приложение, которое обрабатывает аргументы командной строки.

#include <cstdlib>
#include <string>

int runApp(const std::string& args) {
    std::string cmd = "./myapp " + args;
    return std::system(cmd.c_str());
}

TEST(E2ETest, AppReturnsZeroOnValidInput) {
    int exitCode = runApp("--input data.txt --output result.txt");
    EXPECT_EQ(exitCode, 0);
    // Дополнительно проверяем содержимое result.txt
    std::ifstream f("result.txt");
    std::string content((std::istreambuf_iterator<char>(f)), {});
    EXPECT_FALSE(content.empty());
}

Для сложных приложений (например, серверов) используйте Docker Compose для поднятия инфраструктуры. В CI (GitHub Actions, GitLab CI) запускайте e2e тесты отдельным шагом, чтобы не замедлять быстрые unit-тесты.

4. Продвинутые техники: code coverage, property-based testing

Не ограничивайтесь стандартными подходами. Рассмотрим две мощные техники:

  • Code Coverage с gcov и lcov. Собирайте отчёты после каждого прогона тестов. Стремитесь к 80%+ покрытия, но помните: не всё, что покрыто, — протестировано качественно.
  • Property-based testing с библиотекой rapidcheck. Вместо конкретных значений вы описываете свойства, которые должны выполняться для любых входных данных.
#include <rapidcheck.h>
#include <rapidcheck/gtest.h>

RC_GTEST_PROP(ReverseTest, ReverseOfReverseIsOriginal,
              (const std::vector<int>& vec)) {
    auto reversed = vec;
    std::reverse(reversed.begin(), reversed.end());
    std::reverse(reversed.begin(), reversed.end());
    RC_ASSERT(reversed == vec);
}

Такой подход отлавливает редкие граничные случаи, которые вы могли упустить.

Заключение

Продвинутое тестирование в C++ — это не просто запуск тестов, а системный подход: от изолированных юнит-тестов с моками до полноценных e2e сценариев в CI. Используйте Google Test и Google Mock для структурирования, параметризованные тесты для покрытия, а property-based testing для поиска скрытых багов.

Блоги

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