Введение: почему без моков и фикстур тесты — это лотерея
Представьте, что вы пишете код для космического корабля. Ваша функция вычисляет траекторию, обращаясь к модулю управления двигателем. Как проверить, что траектория верна, не запуская настоящий двигатель? В реальном мире это катастрофа. В мире 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++.
Фикстуры решают три задачи:
- Переиспользование кода — один раз настроил, используй во всех тестах.
- Чистота тестов — каждый тест начинается с одинакового состояния.
- Автоматическая очистка — если нужно освободить ресурсы, это делается в деструкторе фикстуры.
А теперь давайте соединим моки и фикстуры в одном примере.
Пишем тест с 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 гарантирует, что перед каждым тестом у нас свежий мок и новый репортёр. Никаких глобальных состояний — каждый тест изолирован.