Мок-объекты и фикстуры в C++ тестах: как писать код без страха

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

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

Начать курс

Введение: почему без моков и фикстур тесты — это лотерея

Представьте, что вы пишете код для космического корабля. Ваша функция вычисляет траекторию, обращаясь к модулю управления двигателем. Как проверить, что траектория верна, не запуская настоящий двигатель? В реальном мире это катастрофа. В мире C++ — это задача для mock-объектов и фикстур.

Если вы когда-нибудь писали юнит-тесты на C++, то наверняка сталкивались с проблемами: тест падает из-за сети, базы данных или файловой системы. Или вы тратите 10 строк на подготовку данных для каждой проверки. Знакомо? Тогда эта статья для вас.

Мы разберём, как mock-объекты в C++ помогают изолировать код от внешнего мира, а фикстуры — переиспользовать настройку тестов. Никакой воды, только код и практика. Поехали!

Что такое mock-объекты и зачем они нужны в C++

Mock-объект (или просто мок) — это замена реального объекта, которая имитирует его поведение. Вы говорите моку: «Когда вызовут метод getData(), верни 42». И он послушно возвращает 42, не обращаясь к базе данных, API или файлу.

В C++ моки особенно важны, потому что язык не прощает ошибок. Если ваш класс зависит от сети, а сеть упала — тест упадёт, хотя ваш код идеален. Моки решают эту проблему: они изолируют тестируемый код.

Основные сценарии использования mock-объектов в C++:

  • Внешние сервисы (базы данных, REST API, файлы).
  • Сложные вычисления, которые замедляют тесты (криптография, рендеринг).
  • Недетерминированные функции (rand(), time()).
  • Проверка взаимодействия: вызывался ли метод, сколько раз, с какими аргументами.

Для создания моков в C++ стандартом де-факто является библиотека Google Mock (она же gmock), которая идёт в комплекте с Google Test. Но можно и написать моки вручную — для небольших проектов это даже проще.

Фикстуры: готовим сцену для теста

Фикстура (test fixture) — это набор объектов и данных, которые нужны для выполнения теста. Вместо того чтобы каждый раз писать:

MyClass obj;
obj.init("data");
obj.connect();

…вы выносите это в фикстуру. Google Test поддерживает фикстуры через наследование от ::testing::Test. Это не просто удобно — это лучшая практика тестирования C++.

Фикстуры решают три задачи:

  1. Переиспользование кода — один раз настроил, используй во всех тестах.
  2. Чистота тестов — каждый тест начинается с одинакового состояния.
  3. Автоматическая очистка — если нужно освободить ресурсы, это делается в деструкторе фикстуры.

А теперь давайте соединим моки и фикстуры в одном примере.

Пишем тест с mock-объектом и фикстурой: реальный пример

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

Шаг 1. Определяем интерфейс

Чтобы подставить мок, нам нужен абстрактный класс. Это ключевой момент: моки работают через полиморфизм.

// weather_api.h
class WeatherAPI {
public:
    virtual ~WeatherAPI() = default;
    virtual double getTemperature(const std::string& city) = 0;
    virtual bool isAvailable() = 0;
};

Шаг 2. Реализуем мок с помощью Google Mock

// mock_weather_api.h
#include <gmock/gmock.h>
#include "weather_api.h"

class MockWeatherAPI : public WeatherAPI {
public:
    MOCK_METHOD(double, getTemperature, (const std::string& city), (override));
    MOCK_METHOD(bool, isAvailable, (), (override));
};

Обратите внимание на макрос MOCK_METHOD. Он говорит gmock: «Создай заглушку для этого метода». Теперь мы можем задать ожидания:

  • EXPECT_CALL(mock, getTemperature("Moscow")).WillOnce(Return(15.5));
  • EXPECT_CALL(mock, isAvailable()).WillRepeatedly(Return(true));

Шаг 3. Создаём фикстуру

Теперь напишем фикстуру, которая будет создавать мок и тестируемый объект.

// test_weather_reporter.cpp
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "mock_weather_api.h"
#include "weather_reporter.h"

using ::testing::Return;

class WeatherReporterTest : public ::testing::Test {
protected:
    // Эта функция вызывается перед каждым тестом
    void SetUp() override {
        mock_api = std::make_shared<MockWeatherAPI>();
        reporter = std::make_unique<WeatherReporter>(mock_api);
    }

    // TearDown вызывается после каждого теста (можно не переопределять)
    void TearDown() override {
        // Здесь можно освободить ресурсы, если нужно
    }

    std::shared_ptr<MockWeatherAPI> mock_api;
    std::unique_ptr<WeatherReporter> reporter;
};

Шаг 4. Пишем тесты

TEST_F(WeatherReporterTest, ReturnsTemperatureForCity) {
    // Задаём поведение мока
    EXPECT_CALL(*mock_api, getTemperature("Moscow"))
        .WillOnce(Return(15.5));

    // Вызываем тестируемый метод
    double temp = reporter->getTemperature("Moscow");

    // Проверяем результат
    EXPECT_DOUBLE_EQ(temp, 15.5);
}

TEST_F(WeatherReporterTest, ThrowsWhenApiUnavailable) {
    EXPECT_CALL(*mock_api, isAvailable())
        .WillOnce(Return(false));

    EXPECT_CALL(*mock_api, getTemperature("Moscow"))
        .Times(0); // Убеждаемся, что getTemperature не вызывается

    EXPECT_THROW(reporter->getTemperature("Moscow"), std::runtime_error);
}

Что здесь происходит? В первом тесте мы говорим: «Когда вызовут getTemperature с "Moscow", верни 15.5». Во втором: «Если API недоступен, метод getTemperature вообще не должен вызываться, а репортёр должен выбросить исключение».

Фикстура WeatherReporterTest гарантирует, что перед каждым тестом у нас свежий мок и новый репортёр. Никаких глобальных состояний — каждый тест изолирован.

Ручные моки: когда

Блоги

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